Первый подход к автоматизации получения https сертификатов через letsencrypt (docker + ansible)

Я считаю что все сайты должны работать по https (и не работать по http).

В конце 2015 года стал публично доступен проект Let's Encrypt. С помощью Let's Encrypt можно бесплатно получить https сертификаты для своих сайтов. Сейчас я плачу по $8 за каждый сертификат в ssls.com и хочу перейти на Let's Encrypt.

Let's Encrypt выдает сертификаты на 90 дней. Они объясняют что они выбрали такой небольшой период для того чтобы пользователи автоматизировали процесс получения нового сертификата. Действительно, до них я получал сертификаты раз в год и этот процесс не был автоматизирован. Руками обновлять сертификаты каждые 90 дней совершенно не хочется, нужно автоматизировать.

У меня очень простая архитектура как работают мои личные сайты:

Архитектура сайтов bessarabov

Есть сервер в котором работает несколько докерных контейнеров. Из каждого контейнера на сервер проброшен порт web сервера. На сервере работает nginx, который слушает 80 и 443 порт и пробрасывает запросы пользователей на эти порты докерных контейнеров.

Вот как-то в эту архитектуру мне нужно вписать автоматическое получение сертификатов с помощью Let's Encrypt.

Перед тем как автоматизировать получение сертификатов нужно сначала получить хотя бы один сертификат руками. Я пошел разбираться как это сделать. Я почитал документации на сайтах:

На мой взгляд, Let's Encrypt как-то очень сложно устроен. Я только частично понимаю как он работает, но в конце-концов мне удалось выполнить действия для того чтобы получить сертификат.

Оказывается, у них есть официальный докерный образ (почему они не пишут об этом сразу? Я бы гораздо быстрее со всем разобрался если бы начал с докера). С докером алгоритм получения сертификата достаточно простой.

Давайте представим что нужно получить https сертификат на сайт example.com. Для этого нужно:

То что для получения сертификата нужно остановить свой сайт и вместо него поднять специальный веб-сервер кажется мне очень странным решением, но для моих личных сайтов это это вполне приемлемо.

Итак, руками сертификат мне получить удалось. Идеальный мир, чтобы сертификаты сами автоматически создавались когда в них будет необходимость, но для начала я хочу сделать выписывание сертификата с помощью одной команды. Для этого я написал простой ansible playbook — он у меня сохранен в файле letsencrypt. Для того чтобы получить (или обновить) сертификат мне нужно указать хост для которого я хочу его получить (для этого нужно заменить строки CHANGEME в файле) и выполнить этот playbook с помощью команды "./letsencrypt".

Вот сходный код:

#!/usr/bin/env ansible-playbook -i hosts

---
- hosts: CHANGEME
  gather_facts: false
  sudo: yes
  vars:
    letsencrypt_host: CHANGEME
    letsencrypt_email: ivan@bessarabov.ru

  tasks:

    - shell: docker pull quay.io/letsencrypt/letsencrypt:latest

    - file: >
        path=/tmp/letsencrypt
        state=absent

    - shell: /etc/init.d/nginx stop

    - shell: docker run \
        --rm \
        --publish 80:80 \
        --publish 443:443 \
        --name letsencrypt \
        --volume "/tmp/letsencrypt/etc/:/etc/letsencrypt" \
        --volume "/tmp/letsencrypt/var/:/var/lib/letsencrypt" \
        quay.io/letsencrypt/letsencrypt:latest \
        certonly --agree-tos --email {{letsencrypt_email}} -d {{letsencrypt_host}}
      ignore_errors: yes

    - file: >
        path=/etc/nginx/ssl/{{letsencrypt_host}}/
        state=directory

    - shell: cp \
        /tmp/letsencrypt/etc/live/{{letsencrypt_host}}/{{item}} \
        /etc/nginx/ssl/{{letsencrypt_host}}/{{item}}
      with_items:
        - fullchain.pem
        - privkey.pem
      ignore_errors: yes

    - file: >
        path=/tmp/letsencrypt
        state=absent

    - shell: /etc/init.d/nginx start

Этот код делает то же самое что раньше я делал руками:

И это работает. С помощью этого playbook я получил несколько сертификатов.

Тут есть тонкости из-за которых я не могу считать этот решение совсем уж хорошим (именно поэтому этот пост называется "Первый подход").

Ansible завершает выполнение playbook сразу после того как команда выдала ошибку. Playbook действует так: остановил nginx, потом пытается получить сертификат, если по как-той причине сертификат не получен, то playbook прекращает работу. Т.е. nginx был остановлен, но снова запущен не был (команда на запуск не отработала, так как выполнение playbook закончилось раньше). Результат — мои сайты не работают. Поэтому я вписал опцию "ignore_errors: yes". Теперь даже если не получилось создать сертификат, то ansible успешно отработает до конца и nginx будет снова запущен.

Как видно из кода этого playbook я создаю сертификат для одного хоста. Чтобы получить сертификат для example.com и для www.example.com мне нужно выполнить этот playbook два раза. У меня есть домен bessarabov.ru на котором работает много сайтов. Я получил через letsencrypt несколько сертификатов для разных хостов на этом домене, но при попытке получить следующий сертификат я получил ошибку:

There were too many requests of a given type ::
Error creating new cert ::
Too many certificates already issued for: bessarabov.ru

На форуме я нашел обсуждение в котором сказано что текущий лимит — 5 сертификатов за 7 дней на одном домене.

Из-за этого ограничения мое решение автоматизации получения сертификата не работает для случаев когда на одном домене есть много хостов. Решение работает если вам нужно получить сертификат только для example.com и www.example.com, но если у вас еще есть домены projecta.example.com, projectb.example.com и для них тоже нужно получить сертификат для www, то это решение уже не подходит. Насколько я понимаю, в такой ситуации нужно получать один сертификат, который выписан сразу на несколько хостов (несколько раз использовать ключ -d), но экспериментов я не проводил.

В этом решении есть еще несколько моментов, которые меня смущают.

У letsencrypt есть есть ключ --renew. Вот, кажется, его использование не изменяет лимиты, возможно стоит запускать докерный образ вместе с ним. И, кажется, (но я совсем не уверен), что останавливать сайт нужно только один раз для получения первого сертификата, а обновление сертификатов можно делать как-то без остановки сайта.

Последний момент, который меня смущает в этом решение — это то что я удаляю всю папку, которую создал letsencrypt (забираю из нее только 2 файла с сертификатами), а letsencrypt при работе пишет что всю эту папку нужно бекапить.

Резюме. Это решение работает в том случае если нужно получать несколько сертификатов для одного домена. Если же сертификатов для одного домена нужно много, то это решение не походит (но, возможно, его можно использовать в качестве базы для нового решения). Нужно либо дорабатывать/менять решение, либо ждать что letsencrypt увеличит свои лимиты.

Иван Бессарабов
ivan@bessarabov.ru

4 января 2016