понедельник, 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.

14 комментариев:

  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/ войти под именем владельца домена, а после успешной авторизации, перейти на страницу получения токена.

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

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

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

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

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

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

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