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

14 KiB
Raw Blame History

Общие сведения

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 часов.