From 36e1865e37faac5f90ed4c451cf59013f1714564 Mon Sep 17 00:00:00 2001 From: bitdeals Date: Thu, 20 Aug 2026 11:53:12 +0000 Subject: [PATCH] feat: CERTBOT_DOMAIN may name several domains certbot's own -d already takes a comma-separated list and puts every name in the certificate's Subject Alternative Names, so `-d "$CERTBOT_DOMAIN"` needed no change at all. What assumed a single domain was everything that treated the variable as a *file name*: --cert-name, and the two scripts that address the lineage under /etc/letsencrypt/live. Certbot names a lineage after the first domain of the list, so all three now take ${CERTBOT_DOMAIN%%,*} instead of the whole string -- otherwise a two-domain value asks for a lineage literally called "a.org,b.org" and the renewal pass looks for a directory nobody made. One certificate with several names rather than several lineages: HAProxy binds a single site.pem, and a second lineage would have nowhere to go. Adding a domain to CERTBOT_DOMAIN on a machine that already holds a certificate still needs one manual issuance -- `certbot renew` reads the names off the certificate it has and never looks at the variable. The README says so, and gives the command. --- README.md | 11 ++++++++--- README.ru-RU.md | 12 +++++++++--- docker/scripts/0-create-cert.sh | 2 +- docker/scripts/1-renew-cert.sh | 2 +- docker/scripts/2-concatenate-cert.sh | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ff7c410..9f053df 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ services: image: registry.bitdeals.org/certbot restart: unless-stopped environment: - - CERTBOT_DOMAIN=example.org + - CERTBOT_DOMAIN=example.org,www.example.org - CERTBOT_EMAIL=admin@example.org # optional, for expiry notices volumes: - certificates:/etc/certificates # shared with haproxy @@ -62,7 +62,7 @@ the one that writes it. ```sh docker run -d \ - -e CERTBOT_DOMAIN=example.org \ + -e CERTBOT_DOMAIN=example.org,www.example.org \ -v certificates:/etc/certificates \ -v letsencrypt:/etc/letsencrypt \ -v letsencrypt_work:/var/lib/letsencrypt \ @@ -103,7 +103,7 @@ Container images are configured using parameters passed at runtime. |Parameter|Function| |:--------|:-------| -|-e CERTBOT_DOMAIN|The domain to certify. Default: empty — no certificate is requested and the site keeps the self-signed placeholder, silently. One domain only; the scripts pass a single `-d`| +|-e CERTBOT_DOMAIN|The domain to certify, or several comma-separated (`a.org,b.org`) — certbot's own `-d` grammar, giving one certificate that carries every name as a Subject Alternative Name. The first domain names the certificate under `/etc/letsencrypt/live`. Default: empty — no certificate is requested and the site keeps the self-signed placeholder, silently| |-e CERTBOT_EMAIL|Address for Let's Encrypt expiry notices. Default: empty, which registers with `--register-unsafely-without-email` and leaves you without warnings — see Notes| |-v /etc/certificates|Shared with HAProxy. Holds `site.pem`: the concatenated certificate and private key HAProxy binds to| |-v /etc/letsencrypt|Certbot's config directory: the ACME account key, the issued certificates and the renewal configuration. Losing it means re-registering and re-issuing| @@ -162,6 +162,11 @@ Container images are configured using parameters passed at runtime. by cron, means a new certbot release reaches the registry — and through Watchtower, production — without anyone triggering a build. Pin a version tag for reproducible builds. +- **Adding a domain to `CERTBOT_DOMAIN` does not reissue by itself.** The loop + calls `certbot renew`, which takes the names from the certificate it already + holds and never reads `CERTBOT_DOMAIN`. On a machine that has one, issue the + new set once by hand and the loop keeps it from then on: + `certbot certonly --standalone -n --agree-tos --http-01-port=380 --cert-name --expand -d `. - **Let's Encrypt enforces rate limits.** Repeated failed issuance against the same domain counts against them; test changes against `--server https://acme-staging-v02.api.letsencrypt.org/directory` before diff --git a/README.ru-RU.md b/README.ru-RU.md index 0f49f7e..145dd79 100644 --- a/README.ru-RU.md +++ b/README.ru-RU.md @@ -43,7 +43,7 @@ services: image: registry.bitdeals.org/certbot restart: unless-stopped environment: - - CERTBOT_DOMAIN=example.org + - CERTBOT_DOMAIN=example.org,www.example.org - CERTBOT_EMAIL=admin@example.org # необязательно, для уведомлений об истечении volumes: - certificates:/etc/certificates # общий с haproxy @@ -63,7 +63,7 @@ volumes: ```sh docker run -d \ - -e CERTBOT_DOMAIN=example.org \ + -e CERTBOT_DOMAIN=example.org,www.example.org \ -v certificates:/etc/certificates \ -v letsencrypt:/etc/letsencrypt \ -v letsencrypt_work:/var/lib/letsencrypt \ @@ -103,7 +103,7 @@ docker push registry.bitdeals.org/certbot |Параметр|Назначение| |:--------|:-------| -|-e CERTBOT_DOMAIN|Домен, на который выпускается сертификат. По умолчанию: пусто — сертификат не запрашивается и сайт молча остаётся с самоподписанной заглушкой. Домен только один: скрипты передают единственный `-d`| +|-e CERTBOT_DOMAIN|Домен, на который выпускается сертификат, либо несколько через запятую (`a.org,b.org`) — это собственный синтаксис `-d` у certbot: получается один сертификат, несущий все имена в Subject Alternative Names. По первому домену назван каталог сертификата под `/etc/letsencrypt/live`. По умолчанию: пусто — сертификат не запрашивается и сайт молча остаётся с самоподписанной заглушкой| |-e CERTBOT_EMAIL|Адрес для уведомлений Let's Encrypt об истечении срока. По умолчанию: пусто, регистрация идёт с `--register-unsafely-without-email`, и предупреждений не будет — см. «Замечания»| |-v /etc/certificates|Общий с HAProxy. Содержит `site.pem` — склеенные сертификат и приватный ключ, которые загружает HAProxy| |-v /etc/letsencrypt|Каталог конфигурации certbot: ключ учётной записи ACME, выпущенные сертификаты и настройки перевыпуска. Его потеря означает повторную регистрацию и повторный выпуск| @@ -166,6 +166,12 @@ docker push registry.bitdeals.org/certbot еженедельной пересборкой по cron означает, что новый выпуск certbot попадает в реестр, а через Watchtower и в продуктив, без того чтобы кто-либо запускал сборку. Для воспроизводимых сборок фиксируйте версию тегом. +- **Добавленный в `CERTBOT_DOMAIN` домен сам по себе не приводит к + перевыпуску.** Цикл вызывает `certbot renew`, а тот берёт имена из уже + выданного сертификата и в `CERTBOT_DOMAIN` не заглядывает. Если сертификат на + машине уже есть, выпустите новый набор имён один раз вручную — дальше цикл + будет его поддерживать: + `certbot certonly --standalone -n --agree-tos --http-01-port=380 --cert-name <первый-домен> --expand -d <новый,список>`. - **У Let's Encrypt есть ограничения частоты.** Повторяющиеся неудачные попытки выпуска на один домен в них засчитываются; проверяйте изменения на `--server https://acme-staging-v02.api.letsencrypt.org/directory`, прежде чем diff --git a/docker/scripts/0-create-cert.sh b/docker/scripts/0-create-cert.sh index 720ba66..49986ee 100644 --- a/docker/scripts/0-create-cert.sh +++ b/docker/scripts/0-create-cert.sh @@ -51,7 +51,7 @@ if [ -n "$CERTBOT_DOMAIN" ]; then certbot certonly --standalone \ --non-interactive --agree-tos --http-01-port=380 \ "$@" \ - --cert-name "$CERTBOT_DOMAIN" \ + --cert-name "${CERTBOT_DOMAIN%%,*}" \ -d "$CERTBOT_DOMAIN" # Concatenate certificates diff --git a/docker/scripts/1-renew-cert.sh b/docker/scripts/1-renew-cert.sh index 7768f63..ae0a1cc 100644 --- a/docker/scripts/1-renew-cert.sh +++ b/docker/scripts/1-renew-cert.sh @@ -1,7 +1,7 @@ #!/bin/sh # Certificates exist -if [ -n "$CERTBOT_DOMAIN" -a -d /etc/letsencrypt/live/"$CERTBOT_DOMAIN" ]; then +if [ -n "$CERTBOT_DOMAIN" -a -d /etc/letsencrypt/live/"${CERTBOT_DOMAIN%%,*}" ]; then # Check certificates and renew them certbot renew --http-01-port=380 diff --git a/docker/scripts/2-concatenate-cert.sh b/docker/scripts/2-concatenate-cert.sh index f838c0b..9ceaa06 100644 --- a/docker/scripts/2-concatenate-cert.sh +++ b/docker/scripts/2-concatenate-cert.sh @@ -8,7 +8,7 @@ # the tests below looking for fullchain.pem in whatever directory the caller # happened to be in. -live="/etc/letsencrypt/live/$CERTBOT_DOMAIN" +live="/etc/letsencrypt/live/${CERTBOT_DOMAIN%%,*}" if [ -f "$live/fullchain.pem" ] && [ -f "$live/privkey.pem" ]; then # Through a temporary file, then an atomic rename: HAProxy reads site.pem at