У бесплатных TLS/SSL сертификатов от CA Let's Encrypt есть особое, не присущее сертификатам других коммерческих CA, достоинство: если однажды получить сертификат от Let's Encrypt, то, при прочих равных, это на года. Не нужно раз в год-два вручную обновлять сертификаты. Не нужно вспоминать что сертификаты где-то есть. Получил, настроил и забыл. Разве не об этом всё всегда мечтали?

Необходимое условие

Инструкции подразумевают что ваш сервер знает свой FQDN.

$ hostname -f
server.example.com
$ hostname
server
$ hostname -d
example.com

Если у вас это не так, следуйте инструкциям по настройке FQDN в Debian.

Установим Certbot

Всё просто.

apt install certbot

Либо используйте apt-get или другой пакетный менеджер вашего дистрибутива.

Установка в других дистрибутивах

Если забыть про пакетные менеджеры, то...

wget -O /usr/local/bin/certbot-auto https://dl.eff.org/certbot-auto
chmod +x /usr/local/bin/certbot-auto
ln -s /usr/local/bin/certbot-auto /usr/local/bin/certbot

Настроим Certbot

Чтобы не писать каждый раз длинную строку из опций, а еще лучше — не вспоминать о них, запишем основные настройки в конфиг Certbot.

mkdir -p /etc/letsencrypt
EMAIL=$(grep -o '\w*@[^\W]*' /etc/aliases | head -1)
tee /etc/letsencrypt/cli.ini <<EOF

# Проверяем владение доменом созданием файлов в видимом из сети каталоге
authenticator = webroot
webroot-path = /var/www/html

# С правилами согласны, уведомления просим присылать на такой-то адрес
agree-tos = True
email = $EMAIL

# Работаем в текстовом режиме без шума и лишних слов
text = True
#quiet = True

# Перезагружаем использующие сертификаты службы после продления
post-hook = service nginx reload; service postfix reload
EOF

Для уведомлений взяли первый адрес из aliases, куда обычно уходит почта в адрес root.

Проверим, что тут нет сюрпризов:

grep email /etc/letsencrypt/cli.ini

Настроим nginx

Исходим из того что у нас на 80 порту нет сайтов, требующих сертификатов, а вся работа по переадресации на HTTPS делается в одном месте.

tee /etc/nginx/conf.d/default.conf <<EOF
server {
    listen $(hostname -f):80 default_server;
    listen [::]:80 default_server;

    location /.well-known {
        root /var/www/html;
    }

    location = /robots.txt {
        return 200 "Host: https://\$host\n";
    }

    location / {
        return 301 https://\$host\$request_uri;
    }
}
EOF

Чтобы не переносить Apache на другой порт будем cлушать только на внешнем IP. Минимальным robots.txt укажем Яндексу на главное зеркало.

Проверим, что требуемые файлы будут видны сертификационному центру.

mkdir -p /var/www/html/.well-known/acme-challenge
echo OK > /var/www/html/.well-known/acme-challenge/example.html
service nginx reload
curl -L http://$(hostname -f)/.well-known/acme-challenge/example.html

Команда должна вывести OK.

Если для вашего сервера есть записи AAAA, то он должен по ним отвечать. Проверим и это.

curl -6 -L http://$(hostname -f)/.well-known/acme-challenge/example.html

Проверочный файл удалим чтобы не смущать Certbot.

rm /var/www/html/.well-known/acme-challenge/example.html
rmdir /var/www/html/.well-known/acme-challenge

Получаем сертификаты

Ввиду лимитов на обращения сначала попробуем получить сертификат для основного домена в режиме «сухого прогона»:

certbot certonly --dry-run -d $(hostname -d) -d www.$(hostname -d)

Этот вызов должен завершиться без сообщений об ошибках.

Если так, то получим сертификат уже в самом деле.

certbot certonly -d $(hostname -d) -d www.$(hostname -d)

Символические ссылки на сертификаты Certbot помещает в каталоги с разбивкой по доменам.

find /etc/letsencrypt/live/ -type l

Проверим полученные сертификаты.

for CN in /etc/letsencrypt/live/*
do 
    echo $CN:
    openssl x509 -text -in $CN/cert.pem | 
        grep -o 'DNS:[^,]*' | 
        cut -f2 -d: | 
        sed 's/^/ - /'
done

Команда должна вывести список доменов для каждого из имеющихся сертификатов.

/etc/letsencrypt/live/example.com:
 - example.com
 - www.example.com

Перейти к настройке nginx.

Скрипт-обёртка для получения сертификатов

Чтобы не занимать в голове место порядком ключей для certbot, добавим обёртку для него.

tee /usr/local/bin/certbot-certonly <<'EOF'
#!/bin/sh
IFS=","
certbot certonly -d "$*";
EOF
chmod +x /usr/local/bin/certbot-certonly

Использовать её очень просто.

certbot-certonly example.com www.example.com

Добавить поддомен в сертификат

Можно добавить до сотни доменов и поддоменов в существующий сертификат.

certbot certonly --expand -d $(hostname -d) \
                          -d www.$(hostname -d) \
                          -d another.example.com \
                          -d ...

Это полезно, если вы забыли добавить поддомен www.

Поддержка старых клиентов

Если вам критически важна поддержка старых клиентов без SNI (напр. IE в WinXP, устаревшие версии Java и Python), то стоит зафиксировать список доменов для сертификата в конфиге.

domains = example.com, www.example.com, another.example.com, ...
expand = True

В этом случае получение сертификатов происходит без явного указания доменов.

certbot certonly --expand

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

true | openssl s_client -showcerts -connect www.example.com:443 2>&1 |
    openssl x509 -text | grep -o 'DNS:[^,]*' | cut -f2 -d:

Потому лучше без него.

Установка сертификатов в nginx

Для nginx нужны три директивы с путями до сертификатов.

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

Скрипт для выделения директив для настройки TLS/SSL во включаемые файлы.

tee /usr/local/bin/update-certs <<'DOC'
#!/bin/bash
mkdir -p /etc/nginx/ssl
for CN in /etc/letsencrypt/live/*
do 
    dest=/etc/nginx/ssl/$(basename $CN).conf
    names=$(openssl x509 -text -in $CN/cert.pem | 
        grep -o 'DNS:[^,]*' | cut -f2 -d: | xargs);
    (
        echo "# include ${dest#/etc/nginx/};"
        echo "# server_name $names;"
        echo "ssl_certificate $CN/fullchain.pem;"
        echo "ssl_certificate_key $CN/privkey.pem;"
        echo "ssl_trusted_certificate $CN/chain.pem;"
        echo
    ) | tee $dest;
done
DOC
chmod +x /usr/local/bin/update-certs

Можно свериться со списком доменов во включаемых файлах.

update-certs
grep -h '^#' /etc/nginx/ssl/*

Полный рабочий пример конфига.

server {
    server_name www.example.com;
    listen 443 ssl default_server;
    # укажем default_server для клиентов без SNI
    listen [::]:443 ssl default_server;
    # слушаем и IPv6 тоже

    # подключим файл с путями к сертификатам и ключу
    include ssl/example.com.conf;

    # исключим возврат на http-версию сайта
    add_header Strict-Transport-Security "max-age=31536000";

    # явно "сломаем" все картинки с http://
    add_header Content-Security-Policy "img-src https: data:; upgrade-insecure-requests";

    # далее все, что вы обычно указываете
    #location / {
    #    proxy_pass ...;
    #}
}

Конфиг для переадресации с голого домена без www:

server {
    server_name example.com;
    listen 443 ssl;
    listen [::]:443 ssl;

    access_log off;

    include ssl/example.com.conf;

    add_header Strict-Transport-Security "max-age=31536000";

    expires max;
    return 301 https://www.$host$request_uri;
}

Perfect Forward Secrecy

Для PFC и высокого результата по тесту следует отказаться от DHE шифров в пользу ECDHE.

tee /etc/nginx/conf.d/ssl_ciphers.conf <<EOF
ssl_ciphers EECDH:+AES256:-3DES:RSA+AES:RSA+3DES:!NULL:!RC4;
EOF

Приоритет для AES256 понижаем для скорости, а 3DES вернём для IE 8.

OCSP stapling

Браузеры кроме Chrome имеют свойство обращаться к CA для сверки со списком отозванных сертификатов, задерживая открытие страниц на секунды. Устранить эту задержку можно прикладывая подписанный ответ OCSP сервера к ответу при создании нового соединения.

tee /etc/nginx/conf.d/ssl_stapling.conf <<EOF
resolver 127.0.0.1;
ssl_stapling on;
ssl_stapling_verify on;
EOF

Если у вас нет локального кеширующего DNS сервера, в директиве resolver следует указать IP фактически используемого DNS сервера.

nameserver=$(grep nameserver /etc/resolv.conf | head -1 | cut -f2 -d" ")
sed -i s/127.0.0.1/$nameserver/ /etc/nginx/conf.d/ssl_stapling.conf
grep resolver /etc/nginx/conf.d/ssl_stapling.conf

Кеширующий сервер

На роль простого кеширующего сервера есть подходящий кандидат.

apt install dnsmasq

Ему нужно запретить отвечать посторонним.

tee /etc/dnsmasq.d/localhost <<EOF
listen-address=127.0.0.1
bind-interfaces
EOF

Применим конфигурацию и проверим работу.

dnsmasq --test
service dnsmasq restart
dig +short example.com @localhost

Проверим OCSP stapling

Обычно всё работает и так, но можно проверить.

for i in 1 2
do
    true | openssl s_client -connect $(hostname):443 -status 2>&1 | 
            grep "OCSP Response Status" || sleep 5
done | tail -1

Успешным считается следующий вывод.

OCSP Response Status: successful (0x0)

Кеш TLS сессий

Кеширование сессий призвано сократить время на установку новых соединений. Кеш сессий будет иметь смысл если параметры каждой сессии хранить заведомо дольше времени жизни соединения (обычно от одной до пяти минут).

tee /etc/nginx/conf.d/ssl_session_cache.conf <<EOF
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 2h;
EOF

По-умолчанию кеш сессий не используется. Это оправдано если у вас нагруженный сервер.

Полный переход на HTTPS

После полного перехода на HTTPS стоит проверить не осталось ли чего на HTTP такого, что должно быть на HTTPS. Также стоит проверить очевидные ошибки конфигурации.

grep :80 /etc/nginx/sites-enabled/*
grep ':443;' /etc/nginx/sites-enabled/*

Продление сертификатов

Если у каждого домена свой сертификат, то для продления сертификатов всё готово.

Если же у вас один сертификат со всеми возможными доменами, и вы не уверены что все домены всегда будут резолвиться на ваш сервер, то необходимо добавить ключ для пропуска ошибок проверки прав на домен при обновлении сертификата.

mkdir -p /etc/systemd/system/certbot.service.d/
tee /etc/systemd/system/certbot.service.d/override.conf <<EOF
[Service]
ExecStart=
ExecStart=/usr/bin/certbot -q renew --allow-subset-of-names
EOF
systemctl daemon-reload

Если этого не сделать и какой-то домен уйдет с вашего сервера, то вы рискуете остаться вообще без сертификатов: по умолчанию Certbot не пытается получить сертификат для неполностью подтвреждённого набора доменов.

Если нет systemd...

Если вы не пользуетесь systemd, то нужно дополнить другой файл:

sed -i 's/-q renew/-q renew --allow-subset-of-names/' /etc/cron.d/certbot
grep --color allow-subset-of-names /etc/cron.d/certbot

Если нет такого файла...

Если вы устанавливали Certbot вручную, то добавим в crontab от root одну строчку:

42 */12 * * * certbot renew --allow-subset-of-names

Нужно заменить 42 в этой строке на случайное число в диапазоне между 0 и 59.

crontab -l | sed s/^42/$((RANDOM % 60))/ | crontab

Certbot будет пытаться обновить сертификаты два раза в день в случайную минуту часа.

Вот и всё

Жаждущим подробностей предлагается обратиться к объяснению особенностей использования сертификатов Let's Encrypt в связке с nginx с разбором каждого шага и детальными пояснениями.