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.
14 KiB
Общие сведения
English version: README.md
Certbot — клиент Let's Encrypt от EFF: получает и перевыпускает бесплатные TLS-сертификаты.
Certbot, работающий в docker-контейнере циклом перевыпуска, в паре с контейнером 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
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
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:
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 пересобирает образ из тех
же исходников. Вручную, если под рукой учётные данные реестра:
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. - Отказ в установке сообщается, а не проглатывается. 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 часов.