понедельник, 2 марта 2020 г.

Автоматическое получение и продление wildcard-сертификатов от Let's Encrypt для доменов, размещённых на DNS Яндекс.Коннект

Получение wildcard-сертификатов от удостоверяющего центра Let's Encrypt возможно только при подтверждение владения доменом, путём создания временной TXT записи вида _acme-challenge.oldfag.ru с определённым значением.
Самое простое решение для автоматизации получения сертификатов от Let’s Encrypt - использование Cetrbot или acme.sh.
Мой выбор пал на Certbot. Он имеет уже готовые плагины для работы с некоторыми DNS-провайдерами. К несчастью, он не имеет плагина для работы с DNS Яндекс.Коннект, но возможность использовать свои скрипты исправляет эту ситуацию.

Acme.sh умеет работать с DNS Яндекс.Коннект, дак зачем же использовать велосипед в виде Certbot и самописных скриптов?
Ответ прост. Проверка существования TXT записи производится запросом к NS серверам домена, в нашем случае dns1.yandex.net и dns2.yandex.net. Вот тут может возникнуть ошибка валидации домена, т.к. на одном из серверов запись уже существует, а на втором ещё нет. Тут нам на помощь и приходит Certbot с самописными скриптами. Для использования собственных скриптов создания DNS-записей у certbot есть 2 ключа:
  • --manual-auth-hook путь к скрипту - для создания TXT записи для авторизации;
  • --manual-cleanup-hook путь к скрипту - для удаления TXT записи после завершения проверки.
Первое, что нужно сделать - установить certbot согласно инструкции для используемого дистрибутива. Второе - получить API-токен для управления DNS зоной. Для этого нужно зайти в панель управления под учётной записью владельца домена. Именно владельца, а не внутреннего администратора. После авторизации нужно перейти на страницу управления токеном и получить его.
Все предварительные этапы завершены, приступим к созданию скриптов для работы с dns-записями. Первым создадим скрипт создания необходимых записей
mkdir -p /scripts/certbot-dns-pddyandex
touch /scripts/certbot-dns-pddyandex/yandex-auth-hook.sh
с таким содержанием
#!/bin/bash
_dir="$(dirname "$0")"
source "$_dir/config.sh"

# Get API key for current domain
API_KEY=${API_KEYMAP["$CERTBOT_DOMAIN"]}
if [ -z "$API_KEY" ]; then
        echo "No API key found for domain $CERTBOT_DOMAIN, exit"
        exit
fi

# Create TXT record
CREATE_DOMAIN="_acme-challenge"
RECORD_ID=$(curl -s -X POST "https://pddimp.yandex.ru/api2/admin/dns/add" \
        -H "PddToken: $API_KEY" \
        -d "domain=$CERTBOT_DOMAIN&type=TXT&content=$CERTBOT_VALIDATION&ttl=3600&subdomain=$CREATE_DOMAIN" \
        | python -c "import sys,json;print(json.load(sys.stdin)['record']['record_id'])")

# Save info for cleanup
if [ ! -d /tmp/CERTBOT_$CERTBOT_DOMAIN ];then
        mkdir -m 0700 /tmp/CERTBOT_$CERTBOT_DOMAIN
        echo $RECORD_ID > /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
else
        echo $RECORD_ID >> /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
fi

# Sleep to make sure the change has time to propagate over to DNS (max: 20 min)
c_time=0
end_time=1200
while [ "$c_time" -le "$end_time" ]; do
        if [ `dig $CREATE_DOMAIN.$CERTBOT_DOMAIN TXT +short @dns1.yandex.net | grep $CERTBOT_VALIDATION` ]; then
                sleep 5
                if [ `dig $CREATE_DOMAIN.$CERTBOT_DOMAIN TXT +short @dns2.yandex.net | grep $CERTBOT_VALIDATION` ]; then
                        sleep 5
                        if [ `dig $CREATE_DOMAIN.$CERTBOT_DOMAIN TXT +short @8.8.8.8 | grep $CERTBOT_VALIDATION` ]; then
                                sleep 5
                                break
                        else
                                sleep 50
                                c_time=$[c_time+50]
                        fi
                else
                        sleep 55
                        c_time=$[c_time+55]
                fi
        else
                sleep 60
                c_time=$[c_time+60]
        fi
done
Он берёт из файла API-токен для конкретного домена и при его помощи создаёт необходимые для проверки TXT записи. Идентификаторы записей для их последующего удаления сохраняются в файле RECORD_ID.
Теперь создадим скрипт для удаления временных записей
touch /scripts/certbot-dns-pddyandex/yandex-cleanup-hook.sh
с таким содержанием
#!/bin/bash
_dir="$(dirname "$0")"
source "$_dir/config.sh"

# Get API key for current domain
API_KEY=${API_KEYMAP["$CERTBOT_DOMAIN"]}
if [ -z "$API_KEY" ]; then
        echo "No API key found for domain $CERTBOT_DOMAIN, exit"
        exit
fi

# Remove the challenge TXT record from the zone
remove_record() {
        RECORD_ID="$1"
if [ -n "${RECORD_ID}" ]; then
        RESULT=$(curl -s -X POST "https://pddimp.yandex.ru/api2/admin/dns/del" \
        -H "PddToken: $API_KEY" \
        -d "domain=$CERTBOT_DOMAIN&record_id=$RECORD_ID" \
            | python -c "import sys,json;print(json.load(sys.stdin)['success'])")
        echo $RESULT
fi
}

if [ -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID ]; then
        while read RECORD; do
                remove_record $RECORD
        done < /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
        rm -f /tmp/CERTBOT_$CERTBOT_DOMAIN/RECORD_ID
fi
Он так же, как и скрипт для создания записей, берёт API-токен для конкретного домена и удаляет записи, идентификаторы которых записаны в файле RECORD_ID.
Последний файл, который осталось создать - хранилище API-токенов
touch /scripts/certbot-dns-pddyandex/config.sh
с таким содержанием
declare -A API_KEYMAP
API_KEYMAP["domain1.tld"]='52 symbols of key ....'
API_KEYMAP["domain2.tld"]='52 symbols of key....'
Все файлы созданы, теперь сделаем файлы скриптов исполняемыми
chmod +x yandex-auth-hook.sh yandex-cleanup-hook.sh
и попробуем получить wildcard-сертификат, для того, чтобы сертификаты не выпускались на самом деле добавим к команде ключ --dry-run
certbot certonly --manual-public-ip-logging-ok --agree-tos --renew-by-default \
-d oldfag.ru -d *.oldfag.ru --manual \
--manual-auth-hook /scripts/certbot-dns-pddyandex/yandex-auth-hook.sh \
--manual-cleanup-hook /scripts/certbot-dns-pddyandex/yandex-cleanup-hook.sh \
--preferred-challenges dns-01 --server https://acme-v02.api.letsencrypt.org/directory \
--register-unsafely-without-email --dry-run
Если всё прошло удачно, то можно выпустить сертификаты уже на самом деле, убрав последний ключ из предыдущей команды.

После того, как сертификат будет успешно выпущен, в каталоге /etc/letsencrypt/renewal будет создан файл с параметрами перевыпуска сертификата. Отслеживая срок действия сертификата в /etc/letsencrypt/live/имя домена/cert.pem можно автоматизировать перевыпуск сертификатов при помощи команды
certbot renew

Для автоматического перевыпуска сертификата, без участия пользователя, нужно в файле /etc/letsencrypt/renewal/домен.conf раскомментировать строку, указывающую оставшийся срок действия сертификата, при достижении которого будет предпринята поптка его обновления
renew_before_expiry = 45 days
Все скрипты и краткая инструкция лежат на GitHub.

33 комментария:

  1. Ругается на ошибку в 17 строке yandex-auth-hook.sh — python: command not found
    И потом (23) Failed writing body
    Но завершается как бы успешно — The dry run was successful
    Просветите, чего он хочет?

    ОтветитьУдалить
    Ответы
    1. Похоже, скрипт не знает, где искать python. Возможно он отсутствует в системе, или сломана ссылка на него.

      Удалить
    2. На новом ubuntu заменить python на python3

      Удалить
    3. Спасибо, помогло заменить на python3
      хотя и без этого работало, но теперь и на ошибку не ругается.

      Удалить
  2. При попытке получить сертификат для поддомена, говорит что не нашел ключ...

    ОтветитьУдалить
    Ответы
    1. Скрипт рассчитан на выпуск wildcard сертификатов для доменов второго уровня. Для доменов третьего и выше уровней можно использовать скрипты из репозитория https://github.com/myallod/certbot-dns-pddyandex

      Удалить
  3. Странно, при попытке получения токена для домена, пишет что Error: no_such_domain хотя домен делегирован на сервера яндекса. Может что-то еще нужно проденлать?

    ОтветитьУдалить
    Ответы
    1. Нужно на страницу https://connect.yandex.ru/ войти под именем владельца домена, а после успешной авторизации, перейти на страницу получения токена.

      Удалить
    2. Спасибо добрый человек, сэномил мне 1 день возни с "Яндексом"

      Удалить
  4. Как-то инструкция слегка неполная без определения когда же заканчивается сертификат, если доменов не один, то надо по крону запускать какой-то скрипт который бы проверял все сертификаты, допустим раз в день и если осталось меньше 3 дней до конца выпускал новый для домена

    ОтветитьУдалить
    Ответы
    1. Почитал требования к продлению и решил что тупо поставлю команду в крон на обновление каждый месяц и норм

      Удалить
    2. На самом деле не нужно создавать отдельное задание в крон. Certbot создаёт задание на обновление сертификатов и сохраняет параметры выпуска сертификатов в /etc/letsencrypt/renewal.
      Срок действия сертификата можно проверять с помощью openssl.

      Удалить
    3. Срок, по наступлении которого нужно перевыпустить сертификат указан в параметре renew_before_expiry = 45 days, который указан в файле /etc/letsencrypt/renewal/домен.conf. По умолчанию он закомментирован, комментарий нужно убрать и указать количество дней до истечения срока действия сертификата, когда его нужно обновить.

      Удалить
    4. О как, спасибо, в статье в конце сбивает с толку что надо что-то отслеживать и автоматизировать, хотя вы говорите что уже всё автоматизировано и надо только в конфиге раскомментировать

      Удалить
  5. огромное спасибо, работает отлично!

    ОтветитьУдалить
  6. Большое спасибо! Есть вопрос: при попытке создать сертификат для нескольких доменов, TXT запись "_acme-challenge" создаётся только у последнего указанного домена

    ОтветитьУдалить
    Ответы
    1. Извиняюсь, запись создалась, но почему то спустя 15 минут

      Удалить
  7. Привет. Скрипт зависает на

    Performing the following challenges:
    dns-01 challenge for домен
    dns-01 challenge for домен

    при этом TXT-запись создаётся.

    ОтветитьУдалить
    Ответы
    1. Скрипт не зависает, а ждёт, пока dns-запись появится на всех 3 dns-серверах, на которых проверяется её существование, 2 публичных dns самого Яндекса и один от Google. Это сделано из-за того, что когда Let's Encrypt пойдёт проверять её наличие на ns-серверах домена, то из-за каких-то настроек синхронизации у самого Яндекса может этой записи не оказаться. По сути дела, этот поиск сделан для того, чтобы удостовериться, что запись доступна из интернета.

      Удалить
    2. спасибо за скрипт, вчера всё получилось. не обратил сразу внимания, что там аж 20 минут ожидания

      Удалить
    3. Сколько не пытался получить за 20 минут, что вручную, что со скриптом - всегда получаю "Запись TXT не найдена". Приходится ждать несколько часов, даже несмотря на то, что dig запись показывает...

      Удалить
    4. Поэтому мне пришлось отказаться от Яндекса и перенести домен на TimeWeb, только тогда прокатило, похоже у Яндекса что-то не то с настройками

      Удалить
    5. В Ubuntu 20.04 всё оказалось намного проще, быстрее и с Яндексом сработало. Вот тут нашла решение: https://serverspace.ru/support/help/lets-encrypt-ubuntu-20-04/

      Удалить
  8. От залетного бро огромное спасибо!!! Выручил

    ОтветитьУдалить
  9. Для тех, кому лень разбираться с версией пайтон, можно заменить
    python -c "import sys,json;print(json.load(sys.stdin)['record']['record_id'])"
    на
    sed -r 's/.*"record_id": ?([0-9]*).*/\1/'
    и
    python -c "import sys,json;print(json.load(sys.stdin)['success'])"
    на
    sed -r 's/.*"success": ?"([a-z]*)".*/\1/'

    ОтветитьУдалить
    Ответы
    1. Ооо, спасибо, минус одна зависимость у скрипта

      Удалить
  10. Тут вдруг штука одна неприятная случилась. Wildcard не обновляется. Все по рецепту было, работало пару лет без претензий. Сегодня вот такое сообщение на выходе:

    Running manual-cleanup-hook command: /scripts/certbot-dns-pddyandex/yandex-cleanup-hook.sh
    Some challenges have failed.
    IMPORTANT NOTES:
    - The following errors were reported by the server:

    Domain: site.ru
    Type: unauthorized
    Detail: During secondary validation: No TXT record found at
    _acme-challenge.site.ru

    Domain: site.ru
    Type: unauthorized
    Detail: No TXT record found at _acme-challenge.site.ru

    To fix these errors, please make sure that your domain name was
    entered correctly and the DNS A/AAAA record(s) for that domain
    contain(s) the right IP address.

    Естественно, токен проверен, ТХТ-записи создаются, А-запись не изменялась (все работает годами без изменений). Есть подозрение что проблема связана с какой-нибудь блокировкой и т.д.

    Подскажешь что-нибудь, хоть копать куда? Спасибо!

    ОтветитьУдалить
    Ответы
    1. Кстати, не-Wildcard'ы обновились за несколько секунд с командой:

      certbot-auto certonly -d site2.ru -d www.site2.ru

      Удалить
    2. У меня 23 мая успешно обновился wildcard, следующий в августе

      Удалить
  11. Добрый день!

    Столкнулся с проблемой при попытке сгенерировать Let's Encrypt Wildcard SSL-сертификат при помощи Certbot. TXT-запись создается и резолвится на стороне dns1.yandex.net, dns2.yandex.net и 8.8.8.8, но в результате выдается ошибка о том, что запись не существует. Пробовали ожидать час, результат такой же. До 1 июня сертификаты генерировались успешно и все работало. Генерация происходит на ОС Ubuntu 22.04.1 LTS, версия Certbot: 1.29.0. Подскажите, у кого-то была такая же проблема? Если да, то удалось ли решить?

    ОтветитьУдалить
  12. Этот комментарий был удален автором.

    ОтветитьУдалить
  13. чтобы не забыть, инструкция для нового api яндекса:
    1) устанавливаем acme.sh
    2) находим файл dns_yandex360.sh, записываем в него токен и orgid:
    export Yandex360_Token="yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    export Yandex360_OrgID="8xxxx"
    и помещаем его в acme.sh/dnsapi

    3)
    /root/acme.sh/acme.sh --issue --server letsencrypt --dns dns_yandex360 -d domain.ru -d '*.domain.ru' --dnssleep 1800

    voila

    ОтветитьУдалить
  14. Здравствуйте. Пытаюсь адаптировать данный скрипт под провайдера beget, возник такой вопрос, при получении сертификата в TXT требуется внести две записи для проверки, у меня вносится одна, прошу подсказать в какую сторону копать.

    ОтветитьУдалить