REG.RU и Let's Encrypt

Множество людей покупают доменные имена у регистраторов и создают собственные сайты. Разумеется, им необходимы SSL‑сертификаты. Однако покупать доверенные сертификаты могут не все и, зачастую, используют бесплатные Let’s Encrypt SSL‑сертификаты, время жизни которых составляет 90 суток.

Для автоматического обновления SSL‑сертификатов Let’s Encrypt обычно используется API управления DNS‑зоной, который предоставляет регистратор доменных имен.

Рассмотрим один из возможных способов автоматического обновления сертификатов на примере регистратора REG.RU и его API управления DNS‑зонами.

Авторизация

Для того чтобы не указывать в скриптах пароли администратора DNS‑зоны в открытом виде, прежде всего, необходимо создать ключ. Сделать это можно с помощью openssl:

openssl req -new -x509 -nodes -sha1 -days 365 -newkey rsa:2048 -keyout my.key -out my.crt

Ключ будет действителен в течение года.

Для того чтобы не вводить при создании нового ключа все данные вручную, можно добавить еще несколько параметров, например, имя организации и e‑mail:

openssl req -new -x509 -nodes -sha1 -days 365 -newkey rsa:2048 -keyout my.key -out my.crt \
        -subj "/C=RU/ST=St.-Petersburg/L=St.-Petersburg/O=my org, Inc/OU=RcL/CN=kx-home.su/emailAddress=myname@mail.ru"

Здесь необходимо заменить значения «my org» и «myname@mail.ru» на ваши собственные.

Для того чтобы загрузить полученный ключ в личном кабинете REG.RU, необходимо выбрать пункт меню 'Настройки':

Рис.1. Настройки.

Рис. 1. Настройки.

далее пункт 'Настройки API':

Рис.2. Настройки API.

Рис. 2. Настройки API.

затем пункт 'Добавить SSL':

Рис.3. Добавить SSL.

Рис. 3. Добавить SSL.

и наконец скопировать текст файла my.crt в поле над кнопкой «Добавить»:

Рис.4. Добавить сертификат.

Рис. 4. Добавить сертификат.

Теперь, для аутентификации администратора DNS‑зоны, в качестве имени пользователя вы можете использовать e‑mail (который указали при создании личного кабинета на сайте REG.RU), а вместо пароля в своих скриптах, — файлы my.key и my.crt.

Автоматизация

Допустим вы имеете домен example.org и поддомен cloud.example.org. Создадим рабочий каталог следующего содержания:

/root/cron.letsencrypt
├── renew-certificates
└── scripts
    ├── _dns.example.org-auth.php
    ├── _dns.example.org-cleanup.php
    ├── cert
    │   ├── my.crt
    │   └── my.key
    ├── cloud.example.org-renew
    └── example.org-renew

В каталоге /root/cron.letsencrypt/scripts/cert/ находятся, созданные нами ранее, сертификат и ключ. Скрипты _dns.example.org‑auth.php и _dns.example.org‑cleanup.php осуществляют создание TXT записи _acme‑challenge.XXXXXXXXXX в DNS‑зоне и ее удаление после обновления SSL-сертификата Let’s Encrypt. Основной скрипт renew‑certificates вызывает поочередно скрипты example.org‑renew и cloud.example.org‑renew, которые, в свою очередь, вызывают утилиту certbot.

Утилита certbot

Скрипт example.org‑renew, для вызова утилиты certbot, выглядит следующим образом:

#!/bin/sh

cwd() {
  pushd `dirname $0` > /dev/nill
  CWD=`pwd -P`
  popd > /dev/null
  echo "$CWD"
}

CWD=`cwd`

certbot certonly -n \
                 --manual \
                 --preferred-challenges=dns \
                 --manual-auth-hook $CWD/_dns.example.org-auth.php \
                 --manual-cleanup-hook $CWD/_dns.example.org-cleanup.php -d example.org

Здесь параметр -d задает имя домена, для которого надо получить SSL‑сертификат. Разумеется, в файле cloud.example.org‑renew будет указан поддомен cloud.example.org.

Параметры ‑‑manual‑auth‑hook и ‑‑manual‑cleanup‑hook указывают на способ создания DNS‑записи _acme‑challenge.XXXXXXXXXX и ее последующего удаления.

Создание TXT записи

Рассмотрим процесс создания TXT записи (_dns.example.org‑auth.php):

#!/usr/bin/php
 $login,
  'domain_name'  => $zone,        /* main domain */
  'subdomain'    => $record_name, /* Name of TXT record */
  'text'         => $certbot_validation,
  'input_format' => 'plain',
];

$sig_params = $params;
sort( $sig_params );

$pkeyid = openssl_pkey_get_private( "file://" . getcwd() . "/cert/kx.key" );
openssl_sign( implode(';', $sig_params), $sig, $pkeyid );

$params['sig'] = base64_encode( $sig );
$url = 'https://api.reg.ru/api/regru2/zone/add_txt';

for( $i = 0; $i < 7; $i++ ) {
  $curl = curl_init();
  curl_setopt( $curl, CURLOPT_URL, $url );
  curl_setopt( $curl, CURLOPT_POST, true );
  curl_setopt( $curl, CURLOPT_SSLCERT, getcwd() . "/cert/kx.crt" );
  curl_setopt( $curl, CURLOPT_SSLKEY, getcwd() . "/cert/kx.key" );
  curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
  curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, 1 );
  curl_setopt( $curl, CURLOPT_POSTFIELDS, $params );

  $result = curl_exec( $curl );
  curl_close( $curl );

  $result = json_decode( urldecode($result), true );
  $status = $result["result"];
  if( strcmp($status, 'success') == 0 ) {
    break;
  }
  sleep( 180 ); /* 3 minute */
}

if( strcmp($status, 'success') !== 0 ) {
  exit( 1 );
}

sleep( 1500 ); /* 25 minutes seems to enough */

exit( 0 );
?>

Утилита certbot передает параметры посредством переменных окружения CERTBOT_DOMAIN и CERTBOT_VALIDATION. Первая из них задает имя домена, а вторая, — уникальный ключ.

Поскольку данный скрипт должен работать одинаково для всех наших доменов и поддоменов, мы должны разобрать доменное имя, задаваемое с помощью опции -d утилиты certbot, на составляющие. Далее сформировать параметры вызова curl чтобы воспользоваться API REG.RU, сделать POST‑запрос и получить вразумительный ответ.

Здесь, по сути, все ясно. Более подробно все наши действия можно разобрать по документации REG.RU.

Основным моментом, на который следует обратить внимание, является цикл наших попыток получить правильный ответ от сервера REG.RU и задержка, которая необходима для того, чтобы утилита certbot успела прочитать DNS‑запись и, тем самым, идентифицировать домен как вашу собственность.

Итак, мы делаем 7 попыток с интервалом 3 минуты на случай, если сайт REG.RU будет тормозить. В случае успешного создания _acme‑challenge.XXXXXXXXXX записи, мы ждем еще 25 минут, так как выгрузка зоны и ее распространение требует времени.

Цифру 25 минут мы подобрали с запасом, проведя несколько экспериментов. Вы можете скорректировать время ожидания так, как посчитаете оптимальным для себя.

Удаление TXT записи

После обновления вашего Let’s Encrypt сертификата,  TXT запись _acme‑challenge.XXXXXXXXXX надо удалить и снова выгрузить зону на всеобщее обозрение. Этим занимается скрипт _dns.example.org‑cleanup.php:

#!/usr/bin/php
 $login,
  'domain_name'  => $zone,        /* main domain */
  'subdomain'    => $record_name, /* Name of TXT record */
  'record_type'  => 'TXT',
  'input_format' => 'plain',
];

$sig_params = $params;
sort( $sig_params );

$pkeyid = openssl_pkey_get_private( "file://" . getcwd() . "/cert/kx.key" );
openssl_sign( implode(';', $sig_params), $sig, $pkeyid );

$params['sig'] = base64_encode( $sig );
$url = 'https://api.reg.ru/api/regru2/zone/remove_record';

for( $i = 0; $i < 3; $i++ ) {
  $curl = curl_init();
  curl_setopt( $curl, CURLOPT_URL, $url );
  curl_setopt( $curl, CURLOPT_POST, true );
  curl_setopt( $curl, CURLOPT_SSLCERT, getcwd() . "/cert/kx.crt");
  curl_setopt( $curl, CURLOPT_SSLKEY, getcwd() . "/cert/kx.key");
  curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
  curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, 1 );
  curl_setopt( $curl, CURLOPT_POSTFIELDS, $params );

  $result = curl_exec( $curl );
  curl_close( $curl );

  $result = json_decode( urldecode($result), true );
  $status = $result["result"];
  if( strcmp($status, 'success') == 0 ) {
    break;
  }
  sleep( 180 ); /* 3 minute */
}

if( strcmp($status, 'success') !== 0 ) {
  exit( 1 );
}

exit( 0 );
?>

Вот, собственно, и всё.

Обновление SSL‑сертификатов

Основной скрипт renew‑certificates может выглядеть следующим образом:

#!/bin/sh

cwd() {
  pushd `dirname $0` > /dev/null
  CWD=`pwd -P`
  popd > /dev/null
  echo "$CWD"
}

CWD=`cwd`

export PYTHONWARNINGS=ignore

LOGFILE=/var/log/renew-example.org-certificates.log

#
# Renew for DNS challenge domains:
#
( cd $CWD/scripts/ ; ./example.org-renew         1> /dev/null 2> /dev/null )
( cd $CWD/scripts/ ; ./cloud.example.org-renew   1> /dev/null 2> /dev/null )

/etc/rc.d/rc.nginx reload    1> /dev/null

if [ "$?" == "0" ]; then
  echo "[`date +'%Y-%m-%d %H:%M:%S'`] Let's Encrypt certificates: RENEWED" >> ${LOGFILE}
else
  echo "[`date +'%Y-%m-%d %H:%M:%S'`] Let's Encrypt certificates: renew FAILED" >> ${LOGFILE}
fi

Его задача состоит в том, чтобы последовательно обновить Let’s Encrypt сертификаты, заставить NGINX, не останавливая свою работу, перечитать конфигурацию и оставить следы наших действий в LOG‑файле.

Периодическое выполнение процедуры обновления сертификатов /root/cron.letsencrypt/renew‑certificates можно поручить cron‑демону или написать systemd‑таймер.

Enjoy.

© Habrahabr.ru