Накануне GA Let's Encrypt: пишем Ansible-роль для certbot и готовим 40+ клиентских сервисов
Let's Encrypt объявил о переходе в General Availability в апреле. Пишем Ansible-роль для certbot через cron, тестируем на staging, планируем покрыть клиентские сервисы к маю.
Let's Encrypt готовится к General Availability в апреле 2016, публикует финальные детали ACME-протокола и снимает ограничения закрытой беты
Let's Encrypt на прошлой неделе опубликовал официальное объявление: General Availability запланирован на апрель. Закрытая бета заканчивается, инвайты больше не нужны, любой домен - просто берёшь и получаешь сертификат. Для нас это не абстрактная хорошая новость, а конкретная точка, к которой нужно подготовиться.
У нас на managed-сопровождении сейчас больше сорока внешних клиентских сервисов. У большинства сертификаты - это либо платный Comodo куплен вручную и однажды просроченный, либо вообще голый HTTP. Мы хотим закрыть всё к маю. Значит, к апрелю нужна рабочая автоматизация, проверенная на staging, а не поделка из bash, которую «как-нибудь допишем».
Что уже есть
В январе мы перевели несколько доменов на Let's Encrypt из закрытой беты - вручную, с прямыми вызовами letsencrypt-auto из ssh. Cron добавили тоже руками на каждом хосте. Это работает, но не масштабируется. Когда нужно покрыть один-два домена и забыть - нормально. Когда нужно раскатать на несколько десятков хостов с разными конфигурациями nginx и при этом иметь возможность что-то поменять централизованно - нужна роль.
Параллельно в марте мы разобрались с Ansible Vault для секретов в CI/CD. Тот же подход - роли, переменные, vault для чувствительного - применим и здесь.
Что делает роль
Роль letsencrypt закрывает три вещи:
- Установка letsencrypt-auto. Берём из официального репозитория EPEL на CentOS или из backports на Debian/Ubuntu - зависит от дистрибутива хоста. Версию фиксируем переменной, чтобы не получать сюрпризов при обновлении пакетов.
- Выпуск сертификата. Certonly в режиме webroot - nginx не останавливается,
.well-known/acme-challenge/отдаётся из выделенной директории. Список доменов передаётся как переменная роли, один хост может иметь несколько записей. - Настройка cron и reload. Задача на обновление запускается дважды в неделю через letsencrypt renew - letsencrypt-auto сам разберётся, нужно ли обновлять (обновляет при остатке менее 30 дней). После renew - reload nginx.
Параметры, которые переопределяются на уровне хоста или группы:
letsencrypt_email: "ops@adg.ru"
letsencrypt_webroot: "/var/www/letsencrypt-challenge"
letsencrypt_domains: []
letsencrypt_cron_hour: "4"
letsencrypt_cron_minute: "30"
letsencrypt_cron_weekday: "2,5"
Никаких хардкодов в задачах - только переменные. Это важно потому, что часть хостов мы не контролируем полностью и там cron или время запуска могут быть жёстко оговорены.
Nginx-конфиг под webroot
Один момент, на который потеряли время на staging: на части хостов nginx уже настроен с редиректом 80 -> 443. В таком случае .well-known/acme-challenge/ никогда не доходит до certbot - nginx редиректит запрос Let's Encrypt на HTTPS раньше, чем тот успевает ответить.
Решение - явное исключение в конфиге виртуального хоста, которое роль тоже умеет прописывать через шаблон:
server {
listen 80;
server_name example.ru www.example.ru;
location ^~ /.well-known/acme-challenge/ {
alias /var/www/letsencrypt-challenge/.well-known/acme-challenge/;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}
На хостах без редиректа роль ставит alias в тот же блок server без дополнительной логики. Шаблон параметризуется переменной letsencrypt_has_https_redirect: true/false.
Тестирование на staging
Let's Encrypt предоставляет отдельный staging-endpoint: https://acme-staging.api.letsencrypt.org. Сертификаты оттуда не валидируются браузерами - они подписаны тестовым CA - но всё остальное работает как в production. Rate limits там значительно мягче.
Включается одним параметром:
letsencrypt_staging: true
Роль в этом случае добавляет --staging флаг к вызовам letsencrypt-auto. Это спасает от ситуации, когда ты прогоняешь роль в третий раз на дне разработки и упираешься в rate limit на production-эндпоинте - что у нас и случилось при первом подходе к написанию роли.
Что пошло не так при разработке
Несколько вещей, которые мы не предусмотрели с первого раза.
Порядок задач имеет значение. Если роль сначала пытается получить сертификат, а webroot-директория ещё не создана или nginx не перечитал конфиг с alias-ом - letsencrypt-auto падает с ошибкой валидации домена. Пришлось добавить явную зависимость: создать директорию, reload nginx, потом letsencrypt-auto.
Идемпотентность letsencrypt renew. letsencrypt renew не выдаёт ошибку, если обновление не нужно - просто пишет «not yet due for renewal» и выходит с кодом 0. Это хорошо. Но letsencrypt certonly при повторном запуске на уже существующий сертификат ведёт себя иначе в зависимости от версии. Добавили условие: выпускать сертификат только если его нет, или явно через --force-renew при нужде.
Кто делает reload. На хостах с несколькими виртуальными хостами reload nginx после renew должен происходить один раз, а не по одному разу на каждый домен. В итоге cron-запись одна на хост, а не на домен.
Состояние на сейчас
Роль написана, прогоняется на staging без ошибок на трёх тестовых хостах с разными конфигурациями: CentOS 7 с одним доменом, Ubuntu 14.04 с тремя доменами и редиректом, Debian 8 с поддоменами. Staging-сертификаты Let's Encrypt получены, cron настроен, nginx перезагружается после renew.
Один незакрытый случай - хосты, где перед nginx стоит HAProxy или другой балансировщик: там webroot-схема потребует отдельного рассмотрения. Несколько таких хостов в списке есть, разберёмся отдельно.
К апрелю планируем прогнать роль по production-хостам в ручном режиме (с --check сначала), получить реальные сертификаты и начать систематически закрывать весь список клиентских сервисов. Сорок с лишним позиций - это несколько дней работы при нормальной автоматизации, не несколько недель.