Files
certbot/README.ru-RU.md
T
bitdeals 55083856c1 docs: add README in English and Russian
Same structure as the bitmessage and bitcoind repositories. The notes carry
what the scripts cannot say for themselves: that the first certificate is a
self-signed placeholder and stays one if issuance fails, that a refused
installation is reported but cannot be repaired, that the scripts are sourced
rather than executed so working directory and set -e are inherited, and that a
stop during issuance can outlast docker's grace period.
2026-08-07 13:39:55 +00:00

173 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Общие сведения
> English version: [README.md](README.md)
[Certbot](https://certbot.eff.org/) — клиент [Let's Encrypt](https://letsencrypt.org/) от EFF: получает и перевыпускает бесплатные TLS-сертификаты.
Certbot, работающий в docker-контейнере циклом перевыпуска, в паре с контейнером [haproxy](https://git.bitdeals.org/private/haproxy), который он снабжает сертификатом.
Репозиторий описывает только развёртывание в docker.
# Использование
Контейнер — не разовая команда. Его команда по умолчанию представляет собой
цикл: запустить скрипт перевыпуска, поспать 12 часов и повторить, — поэтому
сертификат он получает при первом старте и дальше поддерживает свежим.
Каждый проход делает четыре вещи, по скрипту на каждую:
|Скрипт|Что делает|
|:--|:--|
|`entrypoint.sh`|Сам цикл и PID 1 контейнера. Выполняет проход, спит 12 часов, повторяет; неудачный проход не обрывает цикл, а сообщается и повторяется|
|`1-renew-cert.sh`|Начало прохода. Перевыпускает существующий сертификат либо передаёт управление `0-create-cert.sh`, если сертификата ещё нет|
|`0-create-cert.sh`|Первый запуск: пишет самоподписанную заглушку, чтобы HAProxy смог занять 443, дожидается HAProxy и запрашивает настоящий сертификат|
|`2-concatenate-cert.sh`|Склеивает `fullchain.pem` и `privkey.pem` в единый `site.pem`, которого ждёт HAProxy|
|`3-update-haproxy-cert.sh`|Устанавливает `site.pem` в *работающий* HAProxy через его runtime API — без перезапуска и без разрыва соединений|
Проверка владения — **HTTP-01 на порту 380**. Внутри контейнера на нём слушает
собственный standalone-сервер certbot, а HAProxy передаёт туда
`/.well-known/acme-challenge/` с публичного порта 80. Больше до 380 никто
доступа иметь не должен.
Let's Encrypt требует, чтобы публичные записи A/AAAA домена указывали на эту
машину, а порт 80 был доступен из интернета.
## docker-compose
```yaml
services:
certbot:
build:
context: https://git.bitdeals.org/private/certbot.git
dockerfile: ./docker/Dockerfile
image: registry.bitdeals.org/certbot
restart: unless-stopped
environment:
- CERTBOT_DOMAIN=example.org
- CERTBOT_EMAIL=admin@example.org # необязательно, для уведомлений об истечении
volumes:
- certificates:/etc/certificates # общий с haproxy
- letsencrypt:/etc/letsencrypt # ключ учётной записи, сертификаты, настройки перевыпуска
- letsencrypt_work:/var/lib/letsencrypt
volumes:
certificates:
letsencrypt:
letsencrypt_work:
```
`certificates` — тот же том, который HAProxy подключает только на чтение; пишет
в него именно этот контейнер.
## docker cli
```sh
docker run -d \
-e CERTBOT_DOMAIN=example.org \
-v certificates:/etc/certificates \
-v letsencrypt:/etc/letsencrypt \
-v letsencrypt_work:/var/lib/letsencrypt \
registry.bitdeals.org/certbot
```
Цикл перевыпуска — это `CMD` образа, а точка входа базового образа (`certbot`)
сброшена, поэтому всё, что указано после имени образа, заменяет цикл целиком:
разовая команда над тем же состоянием не требует `--entrypoint`:
```sh
docker run --rm \
-v letsencrypt:/etc/letsencrypt \
-v letsencrypt_work:/var/lib/letsencrypt \
registry.bitdeals.org/certbot certbot certificates
```
## Сборка и публикация
Push в `main` собирает и публикует образ (`.gitea/workflows/build.yaml`) с тремя
тегами: `<версия>.<sha7>` — для развёртывания, `<версия>` — для чтения и
`latest` — для compose и Watchtower. Еженедельный cron пересобирает образ из тех
же исходников. Вручную, если под рукой учётные данные реестра:
```sh
docker build . --file docker/Dockerfile --tag registry.bitdeals.org/certbot
docker push registry.bitdeals.org/certbot
```
**Контекст сборки — корень репозитория**, а не `docker/`: Dockerfile копирует
`./docker/scripts/`, поэтому при контексте `./docker` этот каталог не виден и
сборка падает на `COPY`.
# Параметры
Образы контейнера настраиваются параметрами, передаваемыми при запуске.
|Параметр|Назначение|
|:--------|:-------|
|-e CERTBOT_DOMAIN|Домен, на который выпускается сертификат. По умолчанию: пусто — сертификат не запрашивается и сайт молча остаётся с самоподписанной заглушкой. Домен только один: скрипты передают единственный `-d`|
|-e CERTBOT_EMAIL|Адрес для уведомлений Let's Encrypt об истечении срока. По умолчанию: пусто, регистрация идёт с `--register-unsafely-without-email`, и предупреждений не будет — см. «Замечания»|
|-v /etc/certificates|Общий с HAProxy. Содержит `site.pem` — склеенные сертификат и приватный ключ, которые загружает HAProxy|
|-v /etc/letsencrypt|Каталог конфигурации certbot: ключ учётной записи ACME, выпущенные сертификаты и настройки перевыпуска. Его потеря означает повторную регистрацию и повторный выпуск|
|-v /var/lib/letsencrypt|Рабочий каталог certbot. Базовый образ объявляет его как `VOLUME`, поэтому без именованного тома при каждом создании контейнера возникает новый анонимный|
|-p 380|Порт ACME-проверки HTTP-01. Внутренний: на него проксирует HAProxy. Публиковать его на хост не нужно и не следует|
# Замечания
- **Без адреса электронной почты предупреждений об истечении не будет.** Пустой
`CERTBOT_EMAIL` означает регистрацию с `--register-unsafely-without-email`:
если перевыпуск начнёт падать, вы узнаете об этом только когда сертификат
истечёт. Следите за сертификатом внешними средствами или задайте переменную.
- **Первый сертификат самоподписанный, и браузеры об этом скажут.** HAProxy не
стартует без `site.pem`, поэтому `0-create-cert.sh` прежде всего пишет
заглушку. Она заменяется, как только выпущен настоящий сертификат, — но если
выпуск не удался, сайт продолжает отдавать именно заглушку, и сообщение об
этом есть только в журнале контейнера.
- **Приватный ключ попадает к HAProxy через unix-сокет, а не по сети.**
`3-update-haproxy-cert.sh` передаёт весь `site.pem` в runtime API HAProxy —
канал уровня `admin` без аутентификации, — поэтому том с `admin.sock` должен
быть разделён с haproxy и больше ни с кем. TCP-порт был бы доступен любому
контейнеру в общей сети, а правил по портам у docker-сетей нет. См. замечание
о runtime API в [README haproxy](https://git.bitdeals.org/private/haproxy).
- **Отказ в установке сообщается, а не проглатывается.** Runtime API сообщает об
отказе текстом ответа и всё равно закрывает соединение штатно, поэтому код
возврата socat ни о чём не говорит; `3-update-haproxy-cert.sh` вместо этого
разбирает ответы на `set ssl cert` и `commit ssl cert` и останавливается на
первом, который не является подтверждением. Исправить он ничего не может:
`site.pem` на томе в любом случае верен, поэтому отказ означает, что
*работающий* HAProxy остаётся на прежнем сертификате до перезапуска. Об этом
он и пишет.
- **Установка выполняется на каждом проходе, а не при перевыпуске.** Скрипт
склеивает и переустанавливает сертификат независимо от того, сделал ли
`certbot renew` хоть что-нибудь, — дважды в сутки. Вреда нет, но это значит,
что путь «сертификат обновлён» задействуется постоянно и настоящий перевыпуск
ничем не отличается от любого другого прохода. Для этого у certbot есть
`--deploy-hook`.
- **`site.pem` всегда заменяется атомарным переименованием** — и на пути
заглушки, и на пути перевыпуска. HAProxy читает этот файл при старте, а
обычное перенаправление вывода в него оставляет промежуток, в котором на томе
лежит обрезанный PEM, — то есть сертификат, с которым HAProxy откажется
стартовать.
- **Остановка во время выпуска может не уложиться в отведённое docker время.**
POSIX-оболочка выполняет обработчик сигнала только после возврата команды
переднего плана, поэтому SIGTERM, пришедший, пока `certbot certonly`
общается с Let's Encrypt, придерживается до конца этого вызова — дольше, чем
отведённые `docker stop` по умолчанию 10 секунд, после которых контейнер
убивают посреди выпуска. Ничего при этом не портится (состояние в
`/etc/letsencrypt` переживает, следующий проход доделает), но если чистая
остановка важна — задайте сервису `stop_grace_period: 60s`. Сон между
проходами прерываемый и реагирует за миллисекунды.
- **Скрипты подключаются через `.`, а не запускаются отдельным процессом**,
поэтому текущий каталог, `set -e` и всякая оставленная переменная наследуются
следующим скриптом. Именно поэтому они обращаются к файлам по абсолютным
путям, а `cd` и `umask` держат внутри подоболочек; новые пишите по тому же
правилу.
- **Контейнер работает от root**, как и базовый образ, — ему нужно писать в
`/etc/letsencrypt`. Прав здесь никто потом не понижает.
- **Базовый образ не зафиксирован.** `FROM certbot/certbot:latest` вместе с
еженедельной пересборкой по cron означает, что новый выпуск certbot попадает в
реестр, а через Watchtower и в продуктив, без того чтобы кто-либо запускал
сборку. Для воспроизводимых сборок фиксируйте версию тегом.
- **У Let's Encrypt есть ограничения частоты.** Повторяющиеся неудачные попытки
выпуска на один домен в них засчитываются; проверяйте изменения на
`--server https://acme-staging-v02.api.letsencrypt.org/directory`, прежде чем
оставлять цикл повторять их каждые 12 часов.