Шифрование данных в веб-приложениях: настройка HTTPS, HSTS и защита cookies | AdminWiki

Шифрование данных в веб-приложениях: настройка HTTPS, HSTS и защита cookies

17 сентября 2026 13 мин. чтения

Уровни защиты шифрования данных в веб-приложениях

HTTPS шифрует канал между браузером и сервером и не защищает сессию, если cookie с идентификатором уходит без флага Secure. Браузер отправит такую cookie по HTTP при заходе на http-версию сайта, и именно на этом участке она читается открытым текстом. Вторая частая дыра: сервер не объявил HSTS, поэтому первый запрос пользователя идёт по HTTP, где атакующий подменяет ответ и переводит жертву на незашифрованное соединение (SSL stripping).

Защита данных в веб-приложении складывается из пяти слоёв:

  • Транспортное шифрование. TLS 1.2 и TLS 1.3 в составе HTTPS с наборами шифров на базе ECDHE и AES-GCM или ChaCha20-Poly1305.
  • Политика транспорта. Заголовок HSTS (Strict-Transport-Security), который заставляет браузер обращаться к домену только по HTTPS.
  • Защита cookies. Флаги Secure и HttpOnly плюс атрибут SameSite.
  • Контроль смешанного контента. Директива CSP upgrade-insecure-requests и отказ от HTTP-ресурсов на HTTPS-странице.
  • Шифрование самих данных. На стороне сервера (диски, резервные копии, отдельные поля БД) и на стороне клиента через Web Crypto API.

Первые четыре слоя настраиваются в Nginx и Apache и не требуют правок кода приложения. Пятый слой делится на серверное шифрование, где ключами управляет администратор, и клиентское, при котором открытый текст не покидает браузер.

Дальше: выпуск и автопродление сертификатов Let's Encrypt, конфигурации Nginx и Apache с TLS 1.3, включение HSTS без риска заблокировать сайт, установка флагов Secure и HttpOnly, поиск смешанного контента и примеры шифрования в браузере.

Настройка HTTPS с современными шифрами: пошаговое руководство

Let's Encrypt выдаёт бесплатные TLS-сертификаты на 90 дней с автоматическим продлением. Файлы складываются в каталог /etc/letsencrypt/live/<домен>/, где fullchain.pem и privkey.pem остаются символическими ссылками на версии с меткой времени. В конфигурациях веб-сервера указывайте пути до симлинков: при обновлении сами файлы меняются, а ссылки нет, поэтому перезагрузка конфигурации подхватит новый сертификат без правок.

Выпуск сертификата Let's Encrypt через certbot и acme.sh

Самый предсказуемый способ проверки владения доменом - webroot: ACME-клиент кладёт файл в каталог /.well-known/acme-challenge, а веб-сервер отдаёт его валидатору. Для Nginx команда выглядит так:

certbot certonly --webroot --agree-tos --no-eff-email \
  --email admin@example.com \
  --webroot-path /usr/share/nginx/html/ \
  -d example.com -d www.example.com

В конфигурации Nginx для этого каталога нужен отдельный блок, иначе запросы валидатора уйдут в приложение:

location ^~ /.well-known/acme-challenge/ {
    root /usr/share/nginx/html;
    default_type "text/plain";
}

После правки конфигурации примените изменения командой nginx -t && nginx -s reload. Для Apache путь проверки обычно /var/www/html/, а сервис перезапускается через systemctl restart httpd или systemctl restart apache2.

Если домен не должен светить IP сервера или порт 80 закрыт, используйте DNS-валидацию: certbot certonly --manual --agree-tos --email admin@example.com --preferred-challenges=dns -d example.com -d www.example.com. Клиент попросит создать TXT-запись с именем _acme-challenge.example.com и покажет её значение. Домен третьего уровня и выше должен проходить проверку на всех уровнях, то есть для sub.example.com запись создаётся по полному имени.

acme.sh закрывает те же задачи и поддерживает DNS API провайдеров, поэтому продление проходит без ручного создания TXT-записей:

acme.sh --issue -d example.com -d www.example.com --webroot /usr/share/nginx/html
export CA_EMAIL=admin@example.com
acme.sh --issue --dns dns_cf -d example.com

Автоматическое продление ставится в cron. Расписание по понедельникам и четвергам в 00:00 даёт запас времени, если один запуск сорвётся:

0 0 * * 1,4 /usr/bin/certbot renew --noninteractive --deploy-hook "systemctl reload nginx"

certbot продлевает только те сертификаты, до истечения которых осталось менее 30 дней, и обновляет симлинки. Практический нюанс: не ждите 30 дней, продление можно запускать уже через 60 дней после выпуска, тогда окно на исправление ошибок шире. Сервис, который держит сертификат в памяти, получит новый файл только после перезапуска, для этого и нужен deploy-hook вида deploy-hook = systemctl reload nginx в файле обновления.

Использование win-acme для Windows и Docker для автоматизации

На Windows сертификаты выпускает win-acme (wacs.exe). Утилита создаёт каталог проверки, получает сертификат, добавляет привязку на порт 443 в IIS и заводит задание автопродления в планировщике заданий. Для выгрузки в PEM-формат, который понимает Nginx или другой сервис:

wacs.exe --store pemfiles --pemfilespath C:\Certificates --accepttos --emailaddress admin@example.com --host example.com

Если своего веб-сервера нет, а есть Docker, certbot запускается контейнером со встроенным веб-сервером на 80 порту:

docker run -it --rm -p 80:80 \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/lib/letsencrypt:/var/lib/letsencrypt \
  certbot/certbot certonly --standalone -d example.com -d www.example.com

Облачную площадку под такой сервер удобно брать готовой: Timeweb Cloud даёт серверы, VDS/VPS, базы данных, хранилище и Kubernetes, а ресурсы меняются по мере роста нагрузки. Это снимает возню с железом и оставляет время на саму настройку TLS.

Конфигурация Nginx и Apache для современных шифров

Сертификат получен, теперь ограничиваем протоколы и шифры. Для Nginx:

server {
    listen 443 ssl;
    http2 on;
    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_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

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

Для Apache в файле виртуального хоста:

ServerName example.com

SSLEngine on
SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off

SSLUseStapling on
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

Что реально дают эти строки:

ПротоколОбмен ключамиСимметричный шифрКомментарий
TLS 1.3ECDHE (X25519, P-256)AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305Наборы зафиксированы стандартом, порядок задаётся отдельной директивой
TLS 1.2ECDHEAES-GCM, ChaCha20-Poly1305Работает при ssl_prefer_server_ciphers on
TLS 1.1 и 1.0RSA, DHECBC-режимыОтключены в актуальных браузерах
SSLv3 и нижеНетCBCУязвимы к POODLE, держать включёнными нельзя

Проверка после перезагрузки: openssl s_client -connect example.com:443 -tls1 -brief должен завершиться ошибкой, а запрос с -tls1_3 показать успешное рукопожатие. Разбор частых ошибок цепочки сертификатов собран в статье Ручная установка SSL-сертификата на Nginx и Apache, а расширенные настройки reverse proxy и обновления сертификатов описаны в полном руководстве по SSL/TLS и HTTPS в Nginx.

Включение HSTS: защита от downgrade-атак и пошаговая настройка

Заголовок Strict-Transport-Security сообщает браузеру, что домен доступен только по HTTPS. После первого ответа с этим заголовком браузер сам переписывает все http-ссылки на https ещё до отправки запроса и блокирует обход ошибок сертификата: кнопки «всё равно продолжить» не будет. Так закрывается SSL stripping, при котором атакующий подменяет первый HTTP-ответ.

Параметры HSTS: max-age, includeSubDomains, preload

  • max-age - время в секундах, в течение которого браузер помнит политику. 31536000 равно одному году, 300 и 86400 подходят только для тестов.
  • includeSubDomains - распространяет правило на все поддомены. Включайте после проверки, что каждый поддомен имеет валидный сертификат, включая внутренние и служебные хосты.
  • preload - добавляет домен в предзагруженный список браузеров, и правило действует даже при первом визите пользователя. Требования к домену: max-age не меньше 31536000, задан includeSubDomains, HTTPS работает на всех поддоменах, порт 80 корректно отвечает редиректом.

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

Настройка HSTS на Nginx и Apache

В Nginx директива ставится внутри HTTPS-сервера, параметр always гарантирует отправку заголовка и для кодов ошибок:

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

В Apache:

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"

Порядок безопасного включения: max-age=300, затем 86400, затем 31536000. Заголовок отправляется только в ответах по HTTPS, для http-сервера настраивается редирект 301 на https. Проверка: curl -sI https://example.com | grep -i strict-transport-security. Если после включения HSTS сертификат сломается, пользователи увидят ошибку и обойти её не смогут, поэтому мониторинг срока действия сертификата ставят до, а не после. Дополнительные заголовки безопасности и их связка с HSTS разобраны в материале Nginx и Apache: полная защита веб-серверов.

Флаги Secure и HttpOnly для cookies: защита от перехвата и XSS

Флаг Secure запрещает браузеру отправлять cookie по незашифрованному соединению, флаг HttpOnly закрывает доступ к cookie из JavaScript, поэтому вредоносный скрипт при XSS не заберёт идентификатор сессии. Префиксы имён усиливают правила: cookie с именем __Host- обязана иметь Secure, Path=/ и не иметь Domain, а __Secure- требует только Secure. Эти префиксы отсекают попытки подменить cookie с поддомена.

Установка флагов Secure и HttpOnly на Nginx и Apache

Если cookie ставит приложение за обратным прокси, флаги удобно добавлять на уровне Nginx. Начиная с версии 1.19.3 работает директива proxy_cookie_flags:

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_cookie_flags ~ secure httponly samesite=lax;
}

Для старых сборок применяют proxy_cookie_path, но учитывайте побочный эффект: директива переписывает атрибут Path, поэтому cookies с путём, отличным от корня, могут отвязаться.

proxy_cookie_path / "/; Secure; HttpOnly";

В Apache правка заголовка ответа выглядит так:

Header always edit Set-Cookie ^(.*)$ $1;Secure;HttpOnly

Директива добавит флаги ко всем cookies, включая те, где они уже есть, поэтому после применения проверьте ответ curl -sI https://example.com | grep -i set-cookie на дубли вида Secure;Secure. Надёжнее задавать флаги в самом приложении. Для PHP правки в php.ini или в конфиге пула:

session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax

После изменения php.ini перезапустите PHP-FPM, иначе старые настройки останутся в пуле воркеров.

SameSite: дополнительный уровень защиты cookies

Атрибут SameSite управляет отправкой cookie при кросс-сайтовых запросах и снижает риск CSRF. Значения: Strict не отправляет cookie при переходе с другого сайта, Lax отправляет только при навигации верхнего уровня методом GET, None отправляет всегда и требует флага Secure. Браузеры уже применяют Lax по умолчанию для cookies без явного атрибута, но полагаться на это в продакшене не стоит. Итоговая строка для сессионной cookie:

Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=3600

Для платёжных и OAuth-сценариев, где нужен возврат с внешнего домена, проверяйте поведение SameSite=Lax отдельно: часть редиректов выполняется методом POST и cookie не дойдёт.

Предотвращение утечек через смешанный контент

Смешанный контент (Mixed Content) - это загрузка ресурсов по HTTP со страницы, открытой по HTTPS. Активный контент (скрипты, iframe, стили, XHR-запросы) браузеры блокируют, пассивный (картинки, аудио, видео) автоматически поднимают до HTTPS. Проблема не только в предупреждениях: URL ресурса уходит в открытом виде, а если в нём есть токен или идентификатор, он попадает в логи промежуточных узлов.

Поиск смешанного контента начинается с консоли браузера: строки Mixed Content показывают точный адрес ресурса. Массово проверить шаблоны и собранный фронтенд помогает поиск по исходникам:

grep -rn "http://" ./templates ./public --include="*.html" --include="*.js" --include="*.css"

Жёсткий вариант решения - директива Content-Security-Policy, которая переписывает HTTP-запросы на HTTPS или блокирует их полностью:

# Nginx
add_header Content-Security-Policy "upgrade-insecure-requests" always;

# Apache
Header set Content-Security-Policy "upgrade-insecure-requests"

Директива block-all-mixed-content даёт более строгое поведение и запрещает загрузку HTTP-ресурсов вместо их обновления, но в новых браузерах она считается устаревшей, поэтому основной выбор - upgrade-insecure-requests. CSP стоит вводить постепенно: сначала в режиме Content-Security-Policy-Report-Only с параметром report-uri, чтобы собрать нарушения и не сломать работающий функционал. Практические конфигурации CSP и защита динамического контента от XSS описаны в статье Защита приложений с динамическим контентом.

Отдельно проверьте базовые URL в конфигурации приложения и статические шаблоны: абсолютные http-ссылки в них проще заменить на https, чем ловить их в проде. Протокол-независимые ссылки вида //example.com/cdn.js в новом коде не используйте: они зависят от схемы текущей страницы и легко превращаются в HTTP.

Шифрование на стороне браузера с Web Crypto API

Web Crypto API даёт доступ к криптографии через объект window.crypto.subtle (SubtleCrypto). Методы асинхронные, работают на промисах, а сам интерфейс доступен только в защищённом контексте: HTTPS, localhost или file. Ключи генерируются в браузере, открытый текст не уходит на сервер, что даёт end-to-end шифрование и модель zero-knowledge, при которой сервер хранит только шифротекст.

Пример шифрования данных с AES-GCM

AES-GCM обеспечивает и шифрование, и аутентификацию: изменение байта шифротекста приводит к ошибке проверки тега. Пример генерации ключа, шифрования и дешифрования строки:

async function encryptString(plainText, key) {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const encoded = new TextEncoder().encode(plainText);
  const cipherBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
  return {
    iv: Array.from(iv),
    data: Array.from(new Uint8Array(cipherBuf))
  };
}

async function decryptString(payload, key) {
  const iv = new Uint8Array(payload.iv);
  const data = new Uint8Array(payload.data);
  const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
  return new TextDecoder().decode(plainBuf);
}

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

const payload = await encryptString("секретные данные", key);
const restored = await decryptString(payload, key);

Ключ из пароля пользователя получают через PBKDF2 с солью и большим числом итераций, иначе перебор по словарю займёт минуты:

const salt = crypto.getRandomValues(new Uint8Array(16));
const baseKey = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);
const key = await crypto.subtle.deriveKey(
  { name: "PBKDF2", salt, iterations: 600000, hash: "SHA-256" },
  baseKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

Для обмена симметричным ключом с сервером используют RSA-OAEP: им шифруют короткий ключ AES, а не сами данные, потому что длина сообщения ограничена размером модуля (для 3072 бит и SHA-256 это около 318 байт).

const pair = await crypto.subtle.generateKey(
  {
    name: "RSA-OAEP",
    modulusLength: 3072,
    publicExponent: new Uint8Array([1, 0, 1]),
    hash: "SHA-256"
  },
  true,
  ["encrypt", "decrypt"]
);

Правила, которые нарушают чаще всего: не переиспользуйте IV с одним и тем же ключом, храните IV рядом с шифротекстом, не выводите ключи в localStorage и не считайте AES-GCM заменой TLS. Одна ошибка в управлении ключами обнуляет всю схему, поэтому для критичных данных логику проверяют тестами с фиксированными векторами.

Когда использовать Web Crypto API, а когда серверное шифрование

КритерийКлиентское шифрованиеСерверное шифрование
Кто видит открытые данныеТолько браузер пользователяСервер и администраторы
Управление ключамиНа пользователе, потеря ключа означает потерю данныхВ KMS или HSM, есть ротация и резервные копии
Типовые сценарииМенеджеры паролей, защищённые заметки, мессенджерыОбычные веб-приложения, платежи, персональные данные
СложностьВысокая: клиентская криптография и восстановление доступаНиже: шифрование дисков, бэкапов и полей БД

Клиентское шифрование дополняет серверное, а не отменяет его. TLS защищает канал, но трафик расшифровывается на балансировщике, и дальше до приложения данные идут уже открытыми: если сервисы в одной сети, внутренний участок тоже стоит шифровать. Секреты и ключи API храните в переменных окружения или в хранилище секретов, а не в коде и не в репозитории. Когда сервис обращается к внешним моделям, число мест с токенами удобно сократить: AiTunnel даёт единый интерфейс к более чем 200 моделям (GPT, Gemini, Claude) с управлением ключами и бюджетами, оплатой в рублях и совместимостью с библиотеками OpenAI.

Чек-лист и типичные ошибки при настройке шифрования

  1. Выпустить сертификат Let's Encrypt через certbot, acme.sh, win-acme или контейнер certbot и проверить, что в конфигах указаны пути до симлинков.
  2. Настроить автопродление в cron (0 0 * * 1,4 /usr/bin/certbot renew --noninteractive) с deploy-hook на перезагрузку веб-сервера.
  3. Ограничить протоколы до TLS 1.2 и TLS 1.3 и задать современные наборы шифров в Nginx и Apache.
  4. Включить HSTS, начиная с max-age=300, и только после проверки поддоменов добавлять includeSubDomains и preload.
  5. Установить флаги Secure и HttpOnly для cookies на уровне приложения и веб-сервера, добавить SameSite=Lax.
  6. Найти и убрать смешанный контент, включить CSP с upgrade-insecure-requests.
  7. Оценить необходимость клиентского шифрования через Web Crypto API для самых чувствительных данных.

Частые ошибки, которые сводят работу к нулю:

  • HSTS включён с preload до проверки поддоменов, и внутренний сервис без сертификата перестал открываться.
  • Флаги cookies забыты, и сессия уходит по HTTP при заходе на http-версию сайта.
  • TLS 1.0 и 1.1 оставлены ради «старых клиентов», хотя актуальные браузеры их не используют.
  • Автопродление не настроено, мониторинг срока действия отсутствует, сертификат истекает в выходные.
  • Смешанный контент не проверен, скрипт по HTTP блокируется браузером, часть функционала молча ломается.
  • Правка php.ini без перезапуска PHP-FPM, из-за чего новые флаги cookies не применяются.

Проверка результата занимает минуты:

curl -sI https://example.com | grep -i "strict-transport-security"
curl -sI https://example.com | grep -i "set-cookie"
openssl s_client -connect example.com:443 -tls1_2 2>/dev/null | grep -E "Protocol|Cipher"

Дополнительно прогоните домен через сканер SSL Labs и посмотрите оценку заголовков безопасности, а затем зафиксируйте текущее состояние конфигураций: регулярный аудит по чек-листу из материала Аудит безопасности Nginx: TLS, заголовки и контроль доступа покажет, что изменилось после обновлений веб-сервера. Начните с одного шага: проверьте, есть ли в ответе вашего сайта заголовок Strict-Transport-Security и флаг Secure у сессионной cookie. Эти две проверки чаще всего выявляют реальные дыры.

Поделиться:
Сохранить гайд? В закладки браузера