TLS_SSL

TLS/SSL

TLS (Transport Layer Security) — протокол защиты данных при передаче по сети. В HTTPS он обеспечивает шифрование, целостность данных и проверку подлинности сервера. Название SSL всё ещё употребляется, но протоколы SSL устарели; на практике используют TLS 1.2 и TLS 1.3.

HTTP  → обычно TCP/80, без TLS
HTTPS → обычно TCP/443, HTTP внутри TLS

TLS handshake

TLS handshake — начальный этап соединения, на котором клиент и сервер согласовывают защиту и получают сеансовые ключи.

Упрощённая последовательность:

  1. Клиент сообщает поддерживаемые версии TLS, алгоритмы и имя сайта через SNI.
  2. Сервер выбирает параметры и отправляет сертификат.
  3. Клиент проверяет доменное имя, срок действия, подписи и цепочку доверия.
  4. Стороны обмениваются ключевым материалом и получают одинаковые сеансовые ключи.
  5. Последующий HTTP-трафик шифруется быстрым симметричным алгоритмом.

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

SNI (Server Name Indication) передаёт имя сайта в начале TLS-соединения. Благодаря SNI Nginx может обслуживать несколько HTTPS-сайтов с разными сертификатами на одном IP-адресе.


Сертификаты и CA

TLS-сертификат связывает открытый ключ с доменным именем. Обычно он содержит:

CA (Certificate Authority) — удостоверяющий центр, выпускающий и подписывающий сертификаты. Цепочка доверия обычно выглядит так:

Корневой CA
└── Промежуточный CA
    └── Сертификат example.com

Корневой CA уже находится в доверенном хранилище клиента. Сервер должен отправить сертификат сайта и промежуточные сертификаты.

Типичные файлы:

fullchain.pem  # сертификат сайта и промежуточная цепочка
privkey.pem    # закрытый ключ
chain.pem      # промежуточные сертификаты CA

Для Nginx обычно используют:

ssl_certificate     /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;

Закрытый ключ нельзя публиковать или хранить в каталоге статики. Ограничьте доступ:

sudo chown root:root /path/to/privkey.pem
sudo chmod 600 /path/to/privkey.pem

Просмотр сертификата:

openssl x509 -in certificate.pem -text -noout
openssl x509 -in certificate.pem -noout -subject -issuer -dates

Проверка удалённого сайта с передачей SNI:

openssl s_client -connect example.com:443 \
  -servername example.com </dev/null

Проверка дат:

openssl s_client -connect example.com:443 \
  -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates

Self-signed сертификаты

Self-signed certificate подписан собственным ключом, а не публично доверенным CA. Он подходит для локальной разработки, лабораторных стендов и внутренних систем, где собственный CA добавлен в доверенные хранилища клиентов.

Браузер не доверяет произвольному self-signed сертификату и показывает предупреждение. Соединение может быть зашифровано, но подлинность сервера не подтверждена доверенным CA.

Создание сертификата для localhost с SAN:

openssl req -x509 -newkey rsa:2048 -sha256 -nodes \
  -days 365 \
  -keyout local.key \
  -out local.crt \
  -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

Установка файлов:

sudo install -d -m 700 /etc/nginx/tls
sudo install -m 644 local.crt /etc/nginx/tls/local.crt
sudo install -m 600 local.key /etc/nginx/tls/local.key

Конфигурация Nginx:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name localhost;

    ssl_certificate     /etc/nginx/tls/local.crt;
    ssl_certificate_key /etc/nginx/tls/local.key;

    root /var/www/html;
}

Let's Encrypt и Certbot

Let's Encrypt — публичный CA, бесплатно выдающий доменно-проверенные сертификаты через протокол ACME. Основные проверки контроля над доменом:

Для HTTP-01 домен должен указывать на сервер, а TCP-порт 80 должен быть доступен извне. DNS-01 применяется в том числе для wildcard-сертификатов *.example.com.

Certbot — ACME-клиент для получения и обновления сертификатов.

Установка

Debian/Ubuntu:

sudo apt update
sudo apt install certbot python3-certbot-nginx

Fedora:

sudo dnf install certbot python3-certbot-nginx

Arch Linux:

sudo pacman -S certbot certbot-nginx

Названия пакетов могут зависеть от версии дистрибутива.

Подготовка

До выпуска сертификата настройте рабочий HTTP-хост:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    root /var/www/example.com;
    location / {
        try_files $uri $uri/ =404;
    }
}

Проверьте конфигурацию и DNS:

sudo nginx -t
sudo systemctl reload nginx
dig +short example.com A
dig +short example.com AAAA

Ошибочная AAAA-запись может сорвать проверку, если сервер недоступен по IPv6.

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

Получить сертификат и автоматически настроить Nginx:

sudo certbot --nginx \
  -d example.com \
  -d www.example.com

Получить сертификат без изменения конфигурации:

sudo certbot certonly --nginx \
  -d example.com \
  -d www.example.com

Основные файлы обычно расположены здесь:

/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem

Каталог live содержит ссылки на актуальные версии. Не копируйте эти файлы вручную без необходимости: это может нарушить автоматическое обновление.

Webroot

sudo certbot certonly --webroot \
  -w /var/www/letsencrypt \
  -d example.com \
  -d www.example.com

Соответствующий location:

location ^~ /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
    default_type text/plain;
    try_files $uri =404;
}

Wildcard

Wildcard требует DNS-01:

sudo certbot certonly --manual \
  --preferred-challenges dns \
  -d example.com \
  -d '*.example.com'

Ручной режим неудобен для автоматического продления. Для production обычно применяют DNS-плагин с ограниченными API-учётными данными.

*.example.com охватывает api.example.com, но не сам example.com и не a.b.example.com.

Автоматическое обновление

Просмотр сертификатов:

sudo certbot certificates

Тест продления:

sudo certbot renew --dry-run

Проверка systemd timer:

systemctl list-timers | grep -i certbot

При собственной схеме можно использовать deploy hook:

sudo certbot renew \
  --deploy-hook "systemctl reload nginx"

Не запускайте принудительное обновление слишком часто: ACME-сервисы применяют лимиты запросов. Для проверки используйте --dry-run.


HTTPS в Nginx

Минимальная конфигурация:

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name example.com www.example.com;

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

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

После изменения:

sudo nginx -t
sudo systemctl reload nginx
curl -I https://example.com

Не включайте SSLv2, SSLv3, TLS 1.0 и TLS 1.1 без специального требования. Не копируйте устаревшие наборы ssl_ciphers: настройки должны соответствовать версиям Nginx и OpenSSL.

HTTPS reverse proxy

Если Nginx завершает TLS и передаёт запрос приложению по HTTP:

location /api/ {
    proxy_pass http://127.0.0.1:8080/;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

X-Forwarded-Proto сообщает приложению, что исходный запрос использовал HTTPS. Приложение должно доверять proxy-заголовкам только от известного reverse proxy.

Для HTTPS-upstream включите проверку сертификата:

location / {
    proxy_pass https://backend.example.internal;
    proxy_ssl_server_name on;
    proxy_ssl_name backend.example.internal;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
    proxy_ssl_verify_depth 3;
}

Без proxy_ssl_verify on трафик может быть зашифрован, но подлинность upstream не проверяется.


Редирект HTTP → HTTPS

Отдельный виртуальный хост на порту 80:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://$host$request_uri;
}

$request_uri сохраняет путь и строку запроса. Для API можно рассмотреть код 308, который явно сохраняет HTTP-метод и тело:

return 308 https://$host$request_uri;

Поведение всей цепочки клиентов и прокси следует проверить заранее.

Для webroot-проверки можно сделать исключение:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        try_files $uri =404;
    }

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

Если www.example.com перенаправляется на example.com по HTTPS, сертификат всё равно должен быть действителен для www.example.com: TLS handshake выполняется раньше HTTP-редиректа.


HSTS

HSTS (HTTP Strict Transport Security) сообщает браузеру, что сайт необходимо открывать только по HTTPS.

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

Распространение на все поддомены:

add_header Strict-Transport-Security \
    "max-age=31536000; includeSubDomains" always;

Используйте includeSubDomains только тогда, когда все поддомены поддерживают HTTPS.

Вариант для preload:

add_header Strict-Transport-Security \
    "max-age=63072000; includeSubDomains; preload" always;

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

Безопаснее увеличивать срок постепенно:

# Первичная проверка: 5 минут
add_header Strict-Transport-Security "max-age=300" always;

Затем перейти к одному дню и только после стабильной работы — к году. HSTS должен отправляться по HTTPS; заголовок, полученный по HTTP, браузер игнорирует.

Проверка:

curl -I https://example.com

Следует увидеть:

Strict-Transport-Security: max-age=31536000

Учтите наследование add_header: собственный add_header внутри location может отменить наследование заголовков родительского уровня.


Полный пример

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        try_files $uri =404;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

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

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

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

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

    root /var/www/example.com;
    index index.html;

    access_log /var/log/nginx/example.access.log;
    error_log  /var/log/nginx/example.error.log warn;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Применение и проверка:

sudo nginx -t
sudo systemctl reload nginx
curl -I http://example.com
curl -I https://example.com
sudo certbot renew --dry-run

Диагностика

Итоговая конфигурация Nginx:

sudo nginx -T

Журналы:

sudo journalctl -u nginx -n 100 --no-pager
sudo tail -F /var/log/nginx/error.log
sudo less /var/log/letsencrypt/letsencrypt.log

Проверка портов:

sudo ss -lntp | grep -E ':(80|443)\b'

Проверка TLS 1.2 и TLS 1.3:

openssl s_client -connect example.com:443 \
  -servername example.com -tls1_2 </dev/null

openssl s_client -connect example.com:443 \
  -servername example.com -tls1_3 </dev/null

Типичные проблемы

Сертификат не соответствует домену: домен отсутствует в SAN, выбран другой server-блок или клиент не передал SNI.

openssl s_client -connect example.com:443 \
  -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -ext subjectAltName

Неполная цепочка: в ssl_certificate указан сертификат сайта вместо fullchain.pem.

HTTP-01 не проходит: проверьте A/AAAA-записи, порт 80, firewall, server_name, доступ к /.well-known/acme-challenge/ и журналы.

Nginx не читает ключ: проверьте путь и права через sudo nginx -t; не выдавайте всем пользователям доступ к закрытому ключу.

Бесконечный редирект: проверьте TLS termination на внешнем балансировщике и обработку X-Forwarded-Proto.

Mixed content: HTTPS-страница загружает ресурсы по http://; переведите все ресурсы и API на HTTPS.


Рекомендации


Краткая памятка

# Получить сертификат и настроить Nginx
sudo certbot --nginx -d example.com -d www.example.com

# Проверить сертификаты и продление
sudo certbot certificates
sudo certbot renew --dry-run

# Проверить и применить конфигурацию
sudo nginx -t
sudo systemctl reload nginx

# Проверить HTTPS
curl -I https://example.com

# Проверить сертификат сервера
openssl s_client -connect example.com:443 \
  -servername example.com </dev/null