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.
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
# Общие сведения
|
||||
|
||||
> 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 часов.
|
||||
Reference in New Issue
Block a user