certbot + Ansible: TLS-сертификат как одна задача в плейбуке
К концу 2015-го certbot оформился в нормальный инструмент. Написали Ansible-роль - получение и обновление сертификатов теперь одна задача в плейбуке.
Let's Encrypt публичная бета в разгаре - certbot стабилизировался и стал стандартным инструментом для автоматизации TLS
В ноябре мы перевели десяток сервисов на Let's Encrypt за вечер через certbot вручную. Команда за командой, хост за хостом. Работало, но это был не процесс, а «разовая акция с конспектом». К концу декабря накопилось достаточно поводов чтобы сделать это нормально.
Повод первый: новый проект, стандартное managed-сопровождение, пять доменов - и инженер потратил полчаса на certbot вместо того чтобы эти полчаса делать что-то менее механическое. Повод второй: в другом проекте certbot обновился, немного поменялись пути, cron-команда сломалась тихо и без алертов. Выяснилось через две недели. Итого: хватит, пишем роль.
Что было до роли
Схема была незамысловатая: берём чеклист из нашего внутреннего вики, ставим certbot, получаем сертификат вручную, добавляем cron строчку, добавляем location в nginx. Это занимало 20-30 минут на домен, при наличии чеклиста всё делалось правильно. Но чеклист - это не инфраструктура. Это надежда на то что человек ничего не пропустит.
Ещё проблема: certbot renew после обновления требует перезагрузки nginx, но не знает как это сделать. Значит надо делать reload отдельной командой после cron или оборачивать в скрипт. Это просто забывалось.
Ansible-роль: что внутри
Роль решает три задачи: установить certbot, получить сертификат для домена, настроить автоматическое обновление с hook-ом на reload nginx.
Структура переменных минимальная:
# group_vars или host_vars
certbot_email: "ops@example.ru"
certbot_domains:
- name: "example.ru"
webroot: "/var/www/example.ru"
aliases:
- "www.example.ru"
- name: "api.example.ru"
webroot: "/var/www/api"
aliases: []
Задача получения сертификата выглядит так:
- name: obtain certificate
command: >
certbot certonly --webroot
-w {{ item.webroot }}
-d {{ item.name }}
{% for alias in item.aliases %}-d {{ alias }} {% endfor %}
--email {{ certbot_email }}
--agree-tos
--non-interactive
args:
creates: "/etc/letsencrypt/live/{{ item.name }}/fullchain.pem"
with_items: "{{ certbot_domains }}"
Флаг creates делает задачу идемпотентной - если сертификат уже есть, certbot не запускается. Это важно: ansible-playbook запускается регулярно, и не нужно чтобы он каждый раз трогал сертификаты.
Для cron роль раскатывает два файла. Первый - сам таймер:
0 4 * * 2 certbot renew --quiet && systemctl reload nginx
Второй - скрипт проверки что certbot не упал тихо. Просто смотрит на дату последнего выданного сертификата и пишет в syslog если что-то подозрительно старое. Не rocket science, но лучше чем ничего.
nginx и acme-challenge
Отдельная маленькая задача роли - убедиться что в nginx есть правильный location для валидации. Добавляется один раз в общий конфиг-сниппет и подключается инклюдом:
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
Все домены на хосте используют один и тот же root для challenge-файлов, certbot об этом знает через --webroot-path. Это чище чем держать разные директории под разные сайты.
Что не автоматизировали
Два случая выпали из роли намеренно.
Первое - standalone-режим. Когда certbot сам поднимает временный HTTP-сервер на 80-м порту. Это требует останавливать nginx, а останавливать nginx в плейбуке без ручного подтверждения неуютно на production-хосте. Пока оставили как ручной шаг с комментарием в README роли.
Второе - не-nginx стеки. Один клиент держит Apache, у другого Jetty-обёртка (Nexus). Для них роль не работает в текущем виде - hook на reload nginx сломает ситуацию или ничего не сделает. Добавили переменную certbot_reload_command с дефолтом systemctl reload nginx - но тестировали только на nginx.
Как выглядит теперь
Новый проект, managed-сопровождение, пять доменов. В плейбук добавляется один блок ролей:
- role: letsencrypt
vars:
certbot_email: "ops@client.ru"
certbot_domains:
- { name: "client.ru", webroot: "/var/www/client", aliases: ["www.client.ru"] }
- { name: "api.client.ru", webroot: "/var/www/api", aliases: [] }
# ... ещё три
Запуск плейбука - сертификаты получены, cron настроен, nginx перезапущен с нужными путями. Инженер идёт дальше.
Про надёжность certbot на этом этапе
Certbot за последние месяцы заметно повзрослел. Когда мы его первый раз трогали в сентябре в закрытой бете - это был скрипт с кучей зависимостей через pip, который устанавливался в virtualenv и вёл себя иногда непредсказуемо. Сейчас установка через letsencrypt-auto стабилизировалась, скрипт сам разрешает зависимости, поведение предсказуемое.
Одно наблюдение: 90-дневный срок сертификата - это и страховка и тест. Если автоматическое обновление сломалось, у вас есть окно чтобы это заметить и починить до того как сайт упадёт. Три месяца достаточно. Но это работает только если мониторинг на дату истечения реально настроен. У нас - через Zabbix с пороговым алертом за 20 дней.
На хостах managed-сопровождения роль уже раскатана на большинстве серверов. Платные сертификаты остаются только где нужен EV - и таких проектов единицы.
- Let's Encrypt публичная бета: десять сервисов на TLS за один вечер · 3 ноября 2015
- Ansible Galaxy: берём роли с полки вместо написания с нуля · 30 октября 2015