Бандл из ключа и цепочки — вещь, которая либо работает, либо валит рукопожатие в самый неподходящий момент. Причём молча: сервер стартует, а браузер уже потом говорит «не удалось установить защищённое соединение». Cert Unifier 3.1.1 идёт от обратного: проверка ключа сертификата здесь — предусловие записи, а не предупреждение в логе. Сначала сравниваются открытые ключи, и только при совпадении собирается <domain>.pem с режимом 0600. Ниже — как устроено, что показали два прогона (локальный на Git Bash и живой на дереве Certbot из 23 доменов) и на чём спотыкаются при первом запуске.

TL;DR: что это и первая проверка
- Что это. Один Bash-скрипт: обходит Certbot
live/и legacy-раскладкиdomain_leN, проверяет пары «ключ — сертификат». - Что пишет. Один бандл на домен: приватный ключ, затем лист, затем промежуточные. Режим 0600, запись атомарная.
- Что отказывается делать. Не пишет бандл, если ключ не совпал, защищён паролем или отсутствует.
- Чего не делает вообще. Не запрашивает, не продлевает и не отзывает сертификаты, не перезагружает службы, не ходит в сеть.
- Проверка за минуту.
--version, затем--dry-run --verboseпо целевому дереву.
wget -O cert-unifier.sh https://raw.githubusercontent.com/paulmann/cert-unifier/main/cert-unifier.sh
chmod +x cert-unifier.sh
./cert-unifier.sh --version
./cert-unifier.sh --dry-run --verbose /etc/letsencrypt/live
Проблема, которую это решает
Собрать бандл руками — три команды cat. Вопрос не в сложности, а в том, что ошибка здесь не видна.
Первый сценарий: Certbot при продлении перевыпустил ключ, а старый .pem остался. Конфигурация указывает на бандл, ключ в нём от прежнего сертификата — и nginx запускается, но рукопожатие падает. В логах это выглядит как «SSL_do_handshake() failed», причину ищут в сертификате, а дело в ключе.
Второй: legacy-раскладки. У домена рядом лежат example.com_le1.crt, example.com_le2.crt и example.com_le10.crt. Сортировка «по алфавиту» уверенно ставит _le10 перед _le2, и старый сертификат перезаписывает новый. Причём _le10 в старых скриптах не ловился вообще — просто потому, что шаблон ждал одну цифру.
Третий: у части legacy-сертификатов файла .key нет вовсе — ключ лежит отдельно, с именем домена. Скрипт, который не проверяет наличие, соберёт бандл без ключа и завершится кодом 0.
Общая черта всех трёх — тихий отказ. Именно поэтому в 3.1.0 нашлось шесть блокирующих дефектов, о которых честно написано в заметках к релизу: --pfx падал на OpenSSL 3.x, --json обрезал вывод и возвращал 1 при успехе, несовпадающий ключ молча попадал в «проверенный» бандл, сертификат без .key давал бандл без ключа и код 0, поколения _le10 и выше не сопоставлялись, а ключ с паролем подвешивал запуск на приглашении OpenSSL. Версия 3.1.0 так и не была опубликована — 3.1.1 это то же дерево с исправленными блокерами.
Что такое Cert Unifier
flowchart TB
SCAN["обход дерева: live/ Certbot и legacy _leN"] --> PAIR["пара: сертификат + ключ"]
PAIR --> VERIFY{"открытый ключ ключа = открытый ключ сертификата?"}
VERIFY -- нет --> REFUSE["отказ, код 1, бандл не пишется"]
VERIFY -- да --> BUILD["сборка: ключ, лист, цепочка"]
BUILD --> ATOMIC["запись во временный файл, chmod 0600, rename"]
ATOMIC --> PEM["domain.pem рядом с источником или в --output-dir"]
BUILD -. опция .-> PFX["domain.pfx для IIS"]| Свойство | Значение |
|---|---|
| Тип | самодостаточный Bash-скрипт, не служба |
| Требования | Bash 4.0+, OpenSSL 1.1.1 или 3.x, flock желательно |
| Платформы | Linux, macOS, WSL |
| Вход | дерево каталогов с ключами и сертификатами |
| Выход | <domain>.pem (и опционально <domain>.pfx) |
| Сеть | нет доступа |
| Лицензия | MIT |
Скрипт занимает 931 строку и весит 32 150 байт — то есть читается целиком за один вечер, что для утилиты, работающей с приватными ключами, важнее любых обещаний.
Возможности
Проверка
| Что | Как работает |
|---|---|
| Сверка пары | открытый ключ из сертификата против открытого ключа из ключа, а не хеш модуля |
| RSA и EC | оба типа, а не только RSA |
| Отказ | несовпадающий, зашифрованный, нечитаемый или отсутствующий ключ останавливает запись |
| Явный обход | --no-key-validation — единственный способ записать непроверенное |
| Проверка PEM | перед записью оба файла разбираются OpenSSL |
Отдельного разговора стоит сама сверка. Сравнивать хеш модуля — привычный способ, но он знает только RSA: у EC-ключей модуля нет, и такая проверка либо не работает, либо даёт ложный результат. Здесь из сертификата извлекается открытый ключ, из ключа — тоже открытый, и сравниваются их SHA-256. Ошибка в любую сторону — отказ до записи.
Запись
| Что | Как работает |
|---|---|
| Атомарность | сборка во временном файле в целевом каталоге, затем rename |
| Права | umask 077, режим 0600 принудительно |
| Идемпотентность | байт-идентичный бандл не перезаписывается, mtime сохраняется |
| Исправление прав | существующий бандл с режимом 0644 получает 0600 |
| Порядок | ключ, лист, промежуточные — именно в таком порядке |
| PFX | опционально, для IIS и Windows |
Автоматизация
| Что | Как работает |
|---|---|
--dry-run |
проверка и отчёт без записи |
--json |
машиночитаемая сводка в stdout, логи в stderr |
| Коды выхода | 0 — сделано, 1 — отказ, 2 — ничего не найдено, 130 — прерывание |
--check-expiry |
предупреждение о близком истечении |
flock |
блокировка, чтобы два прогона не трогали одно дерево |
Раскладки, которые он понимает
flowchart TD ROOT["корень дерева"] --> CB["Certbot: privkey.pem и fullchain.pem"] ROOT --> LG["legacy: domain_leN.crt, .crtca, .key"] LG --> GEN["выбирается старшее поколение N в каталоге"] CB --> OUT["файл domain.pem"] GEN --> OUT OPT["--tlds, --no-tld-filter, --recursive"] --> ROOT
| Раскладка | Файлы | Результат |
|---|---|---|
| Certbot | privkey.pem, fullchain.pem в каталоге домена |
<domain>.pem рядом с ними |
| Legacy | domain_leN.crt, domain_leN.crtca, domain_leN.key |
<domain>.pem |
| Legacy с общим ключом | domain.key вместо domain_leN.key |
<domain>.pem |
| Вложенное дерево | --recursive |
обход в глубину |
Поколение выбирается по числу, а не по строке: _le10 старше _le2, и именно оно попадёт в бандл. Это ровно та деталь, на которой спотыкались старые самописные скрипты.
Замеры и цифры
Проверено 09.11.2026, двумя прогонами.
Первый — локальный, на Windows в Git Bash (bash 5.3.15, OpenSSL 3.5.9), на подготовленном наборе: корректная пара, несовпадающая пара, ключ с паролем, legacy без файла цепочки.
| Проверка | Результат |
|---|---|
--version |
cert-unifier.sh 3.1.1, MIT, код 0 |
Корректная пара (--dry-run) |
planned: 1, запись не выполняется |
| Несовпадающая пара | Private key does NOT match certificate … refusing to write, код 1 |
| Ключ с паролем | Private key is passphrase-protected … use --no-key-validation, запись не выполнена |
Legacy без .crtca |
Missing chain file for legacy.example.com, код 1 |
flock отсутствует |
предупреждение flock not available — running without lock, работа продолжается |
| Реальная запись | бандл 2 878 байт, первая строка -----BEGIN PRIVATE KEY----- |
| Повторный прогон | Unchanged: … (content identical, mode enforced), mtime не изменился |
Второй — на живом сервере (bash 5.3.9, OpenSSL 3.5.5, flock из util-linux 2.41.3): дерево /etc/letsencrypt/live, 23 каталога доменов.
| Проверка | Результат |
|---|---|
--dry-run --json по всему дереву |
status: success, planned: 23, errors: 0, код 0 |
| Побочный эффект dry-run | новых .pem в живом дереве не появилось |
| Реальный прогон на двух доменах в песочницу | код 0, два бандла, режим 600 |
| Содержимое бандла | 5 блоков BEGIN — ключ, лист, три промежуточных |
| Сверка внутри готового бандла | хеш открытого ключа из сертификата равен хешу из ключа |
| Повторный прогон | Success: 0 updated, 2 unchanged, mtime прежний |
| Срок действия одного из сертификатов | notAfter=Jan 17 01:23:43 2027 GMT |

Что замеры не показывают: как ведут себя права 0600 на Windows — NTFS не хранит POSIX-режимы, и локально stat показывал 644 даже после chmod; настоящая проверка режима возможна только на Linux, и она сделана на сервере. Также не проверялись --pfx (экспорт PKCS#12) и --check-expiry: это отдельные ветки, для них нужен свой набор, а правило «два замера на статью» я соблюдаю.
Сильные стороны продукта
- Проверка до записи, а не после. Отказ происходит раньше, чем испорчен предыдущий бандл.
- Сравнение по открытому ключу. Работает и для RSA, и для EC — в отличие от хеша модуля, который знает только RSA.
- Атомарная запись. Неудачный прогон оставляет прежний бандл целым.
- Идемпотентность с сохранением mtime. Проверено на живом дереве: повтор дал
0 updated, 2 unchanged. - Права 0600 принудительно. Проверено: на Linux бандл получил именно 600, а не «как umask позволит».
- Понятные коды выхода. 0, 1, 2, 130 — их можно ставить в условие скрипта, не разбирая текст логов.
- Ноль сетевой активности. Утилита не может ничего продлить или отозвать — только собрать.
- Читаемый размер. 931 строка: для кода, который держит приватные ключи, это аргумент в пользу доверия.
Установка
wget https://raw.githubusercontent.com/paulmann/cert-unifier/main/cert-unifier.sh
chmod +x cert-unifier.sh
./cert-unifier.sh --version
# системно
sudo install -m 755 cert-unifier.sh /usr/local/bin/cert-unifier
| Требование | Зачем | Проверка |
|---|---|---|
| Bash 4.0+ | массивы, shopt, подстановка процессов |
bash --version |
| OpenSSL 1.1.1 или 3.x | сравнение открытых ключей, PEM, PKCS#12 | openssl version |
flock |
не даёт двум прогонам мешать друг другу | command -v flock |
| Чтение ключей | ключ читается в бандл | права на каталог |
| Запись в целевой каталог | бандл кладётся рядом с источниками | ls -ld |
На macOS системный Bash — 3.2; там нужен brew install bash, иначе скрипт не запустится. В минимальных контейнерах часто нет flock — утилита отработает с предупреждением, но параллельный запуск двух прогонов тогда никем не ограничен.
Настройка
flowchart LR D["пробный прогон"] --> V["проверка ключа и PEM"] V --> R["отчёт плана"] R --> J["сводка JSON в stdout"] V --> W["запись только после проверки"] W --> M["режим 0600 при umask 077"] M --> I["повторный прогон находит файл неизменным"]
| Опция | Что делает | По умолчанию |
|---|---|---|
--dry-run |
проверить и отчитаться, ничего не писать | выключено |
--verbose |
отладочные сообщения | выключено |
--tlds LIST |
разрешённые доменные зоны | ru com |
--no-tld-filter |
обрабатывать все домены | выключено |
--output-dir PATH |
писать бандлы в отдельный каталог | рядом с источниками |
--recursive |
обход вложенных каталогов для legacy | выключено |
--pfx |
дополнительно собрать PKCS#12 | выключено |
--check-expiry |
предупреждать о скором истечении | выключено |
--expiry-days N |
порог предупреждения | 30 |
--json |
машиночитаемая сводка в stdout | выключено |
Переменные окружения: ALLOWED_TLDS, LOG_LEVEL, PFX_PASSWORD, CERT_UNIFIER_LOCK, NO_KEY_VALIDATION.
Рабочий порядок настройки такой: сначала --dry-run --verbose по дереву, потом разбор кодов выхода, и только затем реальная запись. Если в dry-run код 2 — значит ни один домен не прошёл фильтр зон; помочь может --no-tld-filter, но сначала стоит понять, почему домены не подошли.
Коды выхода и что они значат на практике
| Код | Что значит | Что делать |
|---|---|---|
| 0 | всё запрошенное сделано | проверить содержимое бандла |
| 1 | хотя бы одна запись отклонена | читать stderr: причина указана с путём |
| 2 | не найдено ни одного сертификата | проверить корень дерева и --tlds |
| 130 | прерывание INT или TERM | ничего не записано, можно повторить |
Отдельно про код 1: он означает не «всё сломалось», а «часть доменов отброшена». Годные домены при этом обрабатываются — отказ точечный, что и подтвердилось в локальном прогоне: корректная пара попала в planned, а несовпадающая и зашифрованная были отклонены с понятными сообщениями.
Почему порядок «ключ, лист, цепочка» важен
Порядок в бандле не косметика. Большинство серверов ожидают именно его, и отклонение дает ошибку, которую легко принять за неправильный сертификат.
| Служба | Что ожидает | Симптом при другом порядке |
|---|---|---|
| nginx | ключ, затем цепочка | SSL_CTX_use_PrivateKey не находит ключ |
| Apache | отдельные директивы | путаница между SSLCertificateFile и SSLCertificateKeyFile |
| Postfix | один файл с ключом и цепочкой | cannot load Private Key |
| HAProxy | ключ и сертификат в одном файле | отказ при разборе PEM |
Проверка простая: первая строка готового файла — BEGIN PRIVATE KEY (или BEGIN RSA PRIVATE KEY для старых ключей), а число блоков BEGIN равно числу сертификатов в цепочке плюс один. В живом прогоне это дало пять блоков: ключ, лист и три промежуточных — и хеш открытого ключа из сертификата совпал с хешем из ключа, то есть бандл действительно собран из одной пары.
Как встроить в обслуживание
Утилита не должна жить в кроне «на всякий случай»: она не отслеживает изменения и не знает, когда Certbot перевыпустил сертификат. Разумная схема — запуск после продления или вручную по чек-листу.
| Триггер | Команда | Зачем |
|---|---|---|
После certbot renew |
cert-unifier --no-tld-filter /etc/letsencrypt/live |
обновить бандлы под новый материал |
| Перед сменой конфигурации | --dry-run --verbose |
увидеть, что изменится |
| Еженедельно | --dry-run --json |
найти пропавшие ключи и истекшие сроки |
| После миграции сервера | прогон по новому корню | поймать битые симлинки |
Две оговорки. Первая: утилита не перезагружает службы, поэтому «собрал бандл» и «сервер начал использовать новый» — два разных шага. Вторая: --json пишет в stdout только сводку, логи идут в stderr, значит в кроне достаточно перенаправить stderr в файл, а stdout — в разбор.
Где это уместно, а где нет
| Задача | Подходит | Почему |
|---|---|---|
| Собрать бандл из Certbot для nginx/Postfix | да | ровно этот сценарий и заявлен |
Привести в порядок legacy-дерево _leN |
да | поколения выбираются по числу |
| Проверить, не перепутаны ли ключи | да | отказ с указанием домена |
| Экспорт PFX для IIS | да, --pfx |
ветка есть, в этом прогоне не проверялась |
| Продление сертификатов | нет | утилита не работает с ACME |
| Перезагрузка служб после замены | нет | намеренно не делает |
| Ротация ключей | нет | только сборка из того, что есть |
Показательные случаи
| Что видно при работе | Что за этим стоит | Что делать |
|---|---|---|
rc=2 на живом дереве |
передан каталог домена вместо родительского | указать корень live/ |
| Часть доменов пропала из отчёта | фильтр зон по умолчанию ru com |
--no-tld-filter, если это ваши домены |
Cannot verify private key при живых файлах |
в Git Bash выставлена переменная MSYS_NO_PATHCONV |
убрать её для вызова |
| Отчёт пуст после копирования дерева | относительные симлинки Certbot сломались | копировать с cp -aL |
| Код 1 на одном домене из десяти | локальная авария: чужой ключ или пароль | разобрать один домен, остальные уже обработаны |
Предупреждение про flock |
нет util-linux в минимальном образе |
поставить пакет или запускать вручную |
Что изменилось в 3.1.1
По заметкам к релизу, шесть дефектов блокировали публикацию 3.1.0, и все они касались именно тихих ошибок.
| Дефект 3.1.0 | Что стало в 3.1.1 |
|---|---|
--pfx падал на OpenSSL 3.x из-за несуществующей опции |
работает на 1.1.1 и 3.x |
--json обрезал вывод и давал код 1 при успехе |
полная сводка, статусы success, none, error |
| Несовпадающий ключ молча попадал в бандл | проверка до записи, отказ с кодом 1 |
Сертификат без .key давал бандл без ключа и код 0 |
отказ с указанием домена |
Поколения _le10 и выше не ловились |
любое число цифр, старшее поколение побеждает |
| Ключ с паролем вешал запуск на приглашении | распознаётся до вызова OpenSSL, отказ без зависания |
Практический вывод из этого списка простой: все шесть — про то, что ошибка раньше была либо невидимой, либо вела себя хуже ошибки. Версия 3.1.1 превращает их в явные отказы с кодом выхода, и это главное, что изменилось.
Подводные камни
- Корень дерева, а не каталог домена. Если передать путь одного домена Certbot, скрипт ответит кодом 2 — это проверено и описано в AGENTS.md.
- Фильтр зон по умолчанию —
ru com. Домен в другой зоне не попадёт в обработку без--no-tld-filter. - Симлинки в
/etc/letsencrypt/live. При копировании дереваcp -aсохраняет относительные ссылки, и они ломаются: прогон по такой копии даётrc=2и «ничего не найдено». Нуженcp -aL; я поймал это сам и потерял один прогон. - Windows и
MSYS_NO_PATHCONV. Если выставить эту переменную в Git Bash, пути вида/c/Users/...больше не преобразуются для нативногоopenssl.exe, и проверка ключа начинает отказывать даже на корректной паре. Симптом обманчивый: сообщение «Cannot verify private key» при живых файлах. - Права 0600 на NTFS не проверить. Локально
statпоказывает 644 при любомchmod; судить о режиме можно только на Linux. - Бандл заменяет предыдущий. Запись атомарная, но не «накопительная»: сначала
--dry-run, потом запись. - Пароль PFX уходит в командную строку. Он виден в
psна время вызова OpenSSL; надёжнееPFX_PASSWORDиз окружения. flockне обязателен, но нужен. Без него прогоны не блокируются, и в логе появляется предупреждение — на боевом сервере это повод поставитьutil-linux.- Утилита не перезагружает службы. После замены бандла перезапуск nginx или Postfix остаётся вашей задачей.
- Версии 3.1.0 не существует. В заметках к 3.1.1 перечислены шесть блокеров, из-за которых 3.1.0 не опубликовали, — полезное чтение перед доверием к «проверенному» бандлу из старых скриптов.
Чек-лист
- [ ] Проверить
bash --version(4.0+),openssl version(1.1.1 или 3.x), наличиеflock. - [ ] Скачать скрипт из релиза и сверить SHA-256 с
SHA256SUMS.txt. - [ ] Запустить
--versionи--help, сверить вывод. - [ ] Сделать
--dry-run --verboseпо корню дерева, а не по каталогу домена. - [ ] Разобрать код выхода: 0 — писать, 1 — читать причины, 2 — проверить зоны.
- [ ] Выполнить реальный прогон и проверить режим 0600 на Linux.
- [ ] Открыть один бандл и убедиться, что первая строка —
BEGIN PRIVATE KEY. - [ ] Повторить прогон и увидеть
unchangedпри неизменном mtime. - [ ] Перезапустить службу самостоятельно и проверить рукопожатие.
- [ ] Записать версию скрипта и дату прогона в журнал обслуживания.
FAQ
Заменяет ли это Certbot?
Нет. Certbot получает и продлевает сертификаты, Cert Unifier только собирает из них бандлы. Сеть утилита не использует вообще.
Что будет, если ключ не совпадает с сертификатом?
Запись этого домена не произойдёт, в stderr будет причина с путём, код выхода — 1. Прежний бандл останется на месте.
Почему сравнение идёт по открытому ключу, а не по хешу модуля?
Хеш модуля работает только для RSA. Сравнение открытых ключей одинаково годится для RSA и EC — это и проверено на обеих ветках в тестах проекта.
Можно ли обойти проверку ключа?
Да, --no-key-validation. Это явный отказ от гарантии: в лог попадёт предупреждение, а бандл может оказаться нерабочим.
Нужен ли root?
Для чтения приватных ключей Certbot — обычно да. Сама утилита привилегий не требует и ничего, кроме бандлов и файла блокировки, не создаёт.
Что означает код 2?
Ни один сертификат не найден. Чаще всего неверный корень дерева (передан каталог домена вместо родительского) или домен вне зон ru com без --no-tld-filter.
Итог
Cert Unifier 3.1.1 закрывает узкую, но болезненную задачу: собрать бандл и не дать тихо получить нерабочий. Проверка ключа сертификата здесь не предупреждение, а условие записи — на локальном наборе это видно по трём разным отказам (несовпадение, пароль, отсутствие цепочки), а на живом дереве — по 23 запланированным доменам с нулём ошибок и последующей записью с режимом 600 и сохранением mtime при повторе. Ограничения честные: только сборка, без продления и перезапуска служб; ветки --pfx и --check-expiry в этом прогоне не проверялись; на Windows права 0600 не увидеть.
Цитата: Бандл проверяют не тогда, когда он сломался, а до того, как он заменил рабочий.
Связанные материалы
- HTML.PHP: шаблонизатор PHP в одном файле — ещё один продукт, где вся логика живёт в одном файле без зависимостей.
- Multilingual Engine: многоязычный WordPress без WPML и Polylang — как разбирать код перед тем, как на него положиться.
- Bash WP-CLI Update 8.1.0 — Bash-инструмент обслуживания с проверяемыми кодами выхода.
- ProFTPD: настройка без сюрпризов — про сертификаты и тихие ошибки конфигурации.
- github-agent 1.3.0 — проверка репозиториев по формальным признакам, включая манифесты лицензий.
LLM Summary
- Тема:
Cert Unifier3.1.1 — Bash-утилита, которая собирает один PEM-бандл на домен (ключ, лист, промежуточные) из Certbotlive/и legacy-раскладок_leN, с проверкой ключа сертификата до записи. - Главное правило: несовпадающий, зашифрованный, нечитаемый или отсутствующий ключ → отказ, код 1, бандл не пишется; обход только через
--no-key-validation. - Запись: атомарная (временный файл в целевом каталоге и
rename), режим 0600 приumask 077, идемпотентность с сохранением mtime, исправление режима 0644 на существующем бандле. - Замер 09.11.2026, локально (Git Bash, bash 5.3.15, OpenSSL 3.5.9): корректная пара →
planned; несовпадение →Private key does NOT match certificate, код 1; ключ с паролем → отказ; legacy без.crtca→Missing chain file; бандл 2 878 байт, первая строкаBEGIN PRIVATE KEY; повтор →content identical, mode enforced. - Замер 09.11.2026, живой сервер (bash 5.3.9, OpenSSL 3.5.5): dry-run по
/etc/letsencrypt/live→status: success,planned: 23,errors: 0, код 0, новых файлов не создано; реальный прогон на двух доменах → режим 600, 5 блоковBEGIN, повтор0 updated, 2 unchanged, mtime прежний, хеш ключа равен хешу сертификата. - Коды выхода: 0 — сделано, 1 — отказ хотя бы по одной записи, 2 — ничего не найдено, 130 — прерывание.
- Требования: Bash 4.0+, OpenSSL 1.1.1 или 3.x,
flockжелательно; Linux, macOS, WSL; на macOS нужен Bash из Homebrew. - Чего не делает: не продлевает и не отзывает сертификаты, не ходит в сеть, не перезагружает службы, не удаляет исходные файлы.
- Не проверялось в этом прогоне: ветки
--pfxи--check-expiry, поведение на macOS, работа под нагрузкой из нескольких параллельных запусков сflock. - Ссылки: https://github.com/paulmann/cert-unifier, https://github.com/paulmann/cert-unifier/releases/tag/v3.1.1
Промт для агента (установка и настройка)
Готовый промт: скопируйте блок целиком. Шаги и предупреждения взяты из AGENTS.md репозитория (файл есть, проверено HTTP 200) и сверены с собственными прогонами.
Задача: поставить Cert Unifier 3.1.1 и собрать проверенные бандлы сертификатов из существующего дерева Certbot, не испортив рабочие файлы.
Материалы:
- репозиторий: https://github.com/paulmann/cert-unifier
- AGENTS.md: https://github.com/paulmann/cert-unifier/blob/main/AGENTS.md
- README: https://github.com/paulmann/cert-unifier/blob/main/README.md
- релиз: https://github.com/paulmann/cert-unifier/releases/tag/v3.1.1
Шаг 1. Предусловия: bash --version (4.0+), command -v openssl && openssl version (1.1.1 или 3.x), command -v flock.
Шаг 2. Скачать и установить:
wget -O cert-unifier.sh https://raw.githubusercontent.com/paulmann/cert-unifier/main/cert-unifier.sh
chmod +x cert-unifier.sh
sudo install -m 755 cert-unifier.sh /usr/local/bin/cert-unifier
Шаг 3. Сверить вывод: ./cert-unifier.sh --version → "cert-unifier.sh 3.1.1" и "License: MIT"; затем --help, код 0.
Шаг 4. Проверка без записи: ./cert-unifier.sh --dry-run --verbose --no-tld-filter /etc/letsencrypt/live
Шаг 5. Прочитать код выхода: 0 — можно писать; 1 — читать stderr, там домен и причина; 2 — неверный корень дерева или фильтр зон.
Шаг 6. Реальная запись в отдельный каталог на первом проходе: --output-dir /root/cert-bundles
Шаг 7. Проверить результат: stat -c '%a %n' на бандлах (ожидается 600), первая строка файла — BEGIN PRIVATE KEY, число блоков BEGIN равно числу сертификатов в цепочке плюс один.
Шаг 8. Повторить прогон и убедиться, что в выводе "0 updated, N unchanged" и mtime не изменился.
Шаг 9. Перезапустить службу самостоятельно (утилита этого не делает) и проверить рукопожатие.
Шаг 10. Записать версию скрипта, дату и код выхода в журнал обслуживания.
Запрещено:
- запускать запись без предварительного --dry-run: бандл заменяет предыдущий;
- передавать каталог одного домена Certbot как корень: будет код 2;
- использовать --no-key-validation без письменного решения владельца сервиса;
- передавать пароль PFX аргументом --pfx-password: он виден в ps, использовать PFX_PASSWORD;
- ожидать продления, отзыва или перезапуска служб: утилита этого не делает;
- вставлять в отчёт содержимое приватных ключей и пароли.
Что считать успехом: --dry-run дал planned больше нуля при нуле ошибок; реальный прогон дал код 0; бандлы имеют режим 600; повторный прогон сообщил 0 updated; служба после перезапуска отдаёт корректный сертификат.